From cee8aac001d8698d1f0c96a147fc63df00e26ced Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Fri, 31 Jan 2025 15:06:06 -0500 Subject: [PATCH 01/70] config revamp WIP --- config/config.ts | 94 ++++++++++++++++++++ config/configFile.ts | 2 +- config/configUtils.ts | 180 ++++++++++++-------------------------- config/configUtils_OLD.ts | 139 +++++++++++++++++++++++++++++ config/environment.ts | 2 +- constants/config.ts | 4 + types/Accounts.ts | 82 +++-------------- types/Config.ts | 39 +++++---- 8 files changed, 332 insertions(+), 210 deletions(-) create mode 100644 config/config.ts create mode 100644 config/configUtils_OLD.ts diff --git a/config/config.ts b/config/config.ts new file mode 100644 index 00000000..8fd86e24 --- /dev/null +++ b/config/config.ts @@ -0,0 +1,94 @@ +import fs from 'fs'; +import findup from 'findup-sync'; + +import { DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME } from '../constants/config'; +import { HubSpotConfigAccount } from '../types/Accounts'; +import { HubSpotConfig, Environment, ConfigFlag } from '../types/Config'; +import { CmsPublishMode } from '../types/Files'; +import { logger } from '../lib/logger'; +import { i18n } from '../utils/lang'; +import { + getGlobalConfigFilePath, + readConfigFile, + parseConfig, +} from './configUtils'; + +export function getConfigFilePath(): string { + const globalConfigFilePath = getGlobalConfigFilePath(); + + if (fs.existsSync(globalConfigFilePath)) { + return globalConfigFilePath; + } + + const localConfigPath = findup([ + DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME, + DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME.replace('.yml', '.yaml'), + ]); + + if (!localConfigPath) { + throw new Error('@TODO'); + } + + return localConfigPath; +} + +export function getConfig( + configFilePath?: string, + useEnv = false +): HubSpotConfig { + if (configFilePath && useEnv) { + throw new Error('@TODO'); + } + + if (useEnv) { + // do something else + } + + const pathToRead = configFilePath || getConfigFilePath(); + + logger.debug(`@TODOReading config from ${pathToRead}`); + const configFileSource = readConfigFile(pathToRead); + + return parseConfig(configFileSource); +} + +function getConfigIfExists(): HubSpotConfig | undefined {} + +function isConfigValid(config: HubSpotConfig): boolean {} + +function createEmptyConfigFile(): void {} + +function deleteConfigFile(): void {} + +function getConfigAccountById(accountId: number): HubSpotConfigAccount {} + +function getConfigAccountByName(accountName: string): HubSpotConfigAccount {} + +function getConfigDefaultAccount(): HubSpotConfigAccount {} + +function getAllConfigAccounts(): HubSpotConfigAccount[]; + +function getAccountEnvironmentById(accountId: number): Environment {} + +function getDefaultAccountEnvironment(): Environment {} + +function updateConfigAccount( + accoundId: number, + fieldsToUpdate: object +): HubSpotConfigAccount {} + +function setConfigAccountAsDefault(accountId: number): void {} + +function renameConfigAccount(accountId: number, newName: string): void {} + +function removeAccountFromConfig(accountId: number): void {} + +function updateHttpTimeout(timeout: number): void {} + +function updateAllowUsageTracking(isAllowed: boolean): void {} + +function updateDefaultCmsPublishMode(cmsPublishMode: CmsPublishMode): void {} + +function isConfigFlagEnabled(flag: ConfigFlag): boolean {} + +function isUsageTrackingAllowed(): boolean {} diff --git a/config/configFile.ts b/config/configFile.ts index da108a7a..20c3a5fd 100644 --- a/config/configFile.ts +++ b/config/configFile.ts @@ -7,7 +7,7 @@ import { HUBSPOT_CONFIGURATION_FILE, HUBSPOT_CONFIGURATION_FOLDER, } from '../constants/config'; -import { getOrderedConfig } from './configUtils'; +import { getOrderedConfig } from './configUtils_OLD'; import { CLIConfig_NEW } from '../types/Config'; import { i18n } from '../utils/lang'; import { FileSystemError } from '../models/FileSystemError'; diff --git a/config/configUtils.ts b/config/configUtils.ts index 04c42265..5b1b83d1 100644 --- a/config/configUtils.ts +++ b/config/configUtils.ts @@ -1,139 +1,73 @@ -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'; +import path from 'path'; +import os from 'os'; +import fs from 'fs'; +import yaml from 'js-yaml'; -export function getOrderedAccount( - unorderedAccount: CLIAccount_NEW -): CLIAccount_NEW { - const { name, accountId, env, authType, ...rest } = unorderedAccount; +import { + HUBSPOT_CONFIGURATION_FOLDER, + HUBSPOT_CONFIGURATION_FILE, +} from '../constants/config'; +import { HubSpotConfig, DeprecatedHubSpotConfigFields } from '../types/Config'; +import { FileSystemError } from '../models/FileSystemError'; +import { logger } from '../lib/logger'; - return { - name, - accountId, - env, - authType, - ...rest, - }; +export function getGlobalConfigFilePath(): string { + return path.join( + os.homedir(), + HUBSPOT_CONFIGURATION_FOLDER, + HUBSPOT_CONFIGURATION_FILE + ); } -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), - }; -} +export function readConfigFile(configPath: string): string { + let source = ''; -function generatePersonalAccessKeyAccountConfig({ - accountId, - personalAccessKey, - env, -}: PersonalAccessKeyOptions): PersonalAccessKeyAccount_NEW { - return { - authType: PERSONAL_ACCESS_KEY_AUTH_METHOD.value, - accountId, - personalAccessKey, - env, - }; -} + try { + source = fs.readFileSync(configPath).toString(); + } catch (err) { + logger.debug('@TODO Error reading'); + throw new FileSystemError( + { cause: err }, + { + filepath: configPath, + operation: 'read', + } + ); + } -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, - }; + return source; } -function generateApiKeyAccountConfig({ - accountId, - apiKey, - env, -}: APIKeyOptions): APIKeyAccount_NEW { - return { - authType: API_KEY_AUTH_METHOD.value, - accountId, - apiKey, - env, - }; -} +export function normalizeParsedConfig( + parsedConfig: HubSpotConfig & DeprecatedHubSpotConfigFields +): HubSpotConfig { + if (parsedConfig.portals) { + parsedConfig.accounts = parsedConfig.portals.map(account => { + account.accountId = account.portalId; + return account; + }); + } -export function generateConfig( - type: AuthType, - options: PersonalAccessKeyOptions | OAuthOptions | APIKeyOptions -): CLIConfig_NEW | null { - if (!options) { - return null; + if (parsedConfig.defaultPortal) { + parsedConfig.defaultAccount = parsedConfig.defaultPortal; } - 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 (parsedConfig.defaultMode) { + parsedConfig.defaultCmsPublishMode = parsedConfig.defaultMode; } - if (configAccount) { - config.accounts.push(configAccount); + 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('@TODO Error parsing', { cause: err }); } - return config; + return normalizeParsedConfig(parsedYaml); } diff --git a/config/configUtils_OLD.ts b/config/configUtils_OLD.ts new file mode 100644 index 00000000..04c42265 --- /dev/null +++ b/config/configUtils_OLD.ts @@ -0,0 +1,139 @@ +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/environment.ts b/config/environment.ts index 6062261f..b345ad80 100644 --- a/config/environment.ts +++ b/config/environment.ts @@ -11,7 +11,7 @@ import { PERSONAL_ACCESS_KEY_AUTH_METHOD, OAUTH_SCOPES, } from '../constants/auth'; -import { generateConfig } from './configUtils'; +import { generateConfig } from './configUtils_OLD'; import { getValidEnv } from '../lib/environment'; import { i18n } from '../utils/lang'; diff --git a/constants/config.ts b/constants/config.ts index d65b7d18..3fca6cac 100644 --- a/constants/config.ts +++ b/constants/config.ts @@ -22,3 +22,7 @@ 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', +} as const; diff --git a/types/Accounts.ts b/types/Accounts.ts index 4e62163a..ab94d09e 100644 --- a/types/Accounts.ts +++ b/types/Accounts.ts @@ -5,7 +5,7 @@ import { ValueOf } from './Utils'; export type AuthType = 'personalaccesskey' | 'apikey' | 'oauth2'; -export interface CLIAccount_NEW { +export interface HubSpotConfigAccount { name?: string; accountId: number; accountType?: AccountType; @@ -23,25 +23,9 @@ export interface CLIAccount_NEW { personalAccessKey?: string; } -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 DeprecatedHubSpotConfigAccountFields = { + portalId: number; +}; export type GenericAccount = { portalId?: number; @@ -56,22 +40,12 @@ export type TokenInfo = { refreshToken?: string; }; -export interface PersonalAccessKeyAccount_NEW extends CLIAccount_NEW { +export interface PersonalAccessKeyConfigAccount extends HubSpotConfigAccount { authType: 'personalaccesskey'; 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 { +export interface OAuthConfigAccount extends HubSpotConfigAccount { authType: 'oauth2'; auth: { clientId?: string; @@ -81,40 +55,12 @@ export interface OAuthAccount_NEW extends CLIAccount_NEW { }; } -export interface OAuthAccount_DEPRECATED extends CLIAccount_DEPRECATED { - authType: 'oauth2'; - auth: { - clientId?: string; - clientSecret?: string; - scopes?: Array; - tokenInfo?: TokenInfo; - }; -} - -export type OAuthAccount = OAuthAccount_NEW | OAuthAccount_DEPRECATED; - -export interface APIKeyAccount_NEW extends CLIAccount_NEW { - authType: 'apikey'; - apiKey: string; -} - -export interface APIKeyAccount_DEPRECATED extends CLIAccount_DEPRECATED { +export interface APIKeyConfigAccount extends HubSpotConfigAccount { 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 { +export interface HubSpotConfigAccountOptions extends HubSpotConfigAccount { tokenInfo?: TokenInfo; clientId?: string; clientSecret?: string; @@ -123,10 +69,6 @@ export interface FlatAccountFields_DEPRECATED extends CLIAccount_DEPRECATED { personalAccessKey?: string; } -export type FlatAccountFields = - | FlatAccountFields_NEW - | FlatAccountFields_DEPRECATED; - export type ScopeData = { portalScopesInGroup: Array; userScopesInGroup: Array; @@ -148,10 +90,10 @@ export type EnabledFeaturesResponse = { enabledFeatures: { [key: string]: boolean }; }; -export type UpdateAccountConfigOptions = - Partial & { - environment?: Environment; - }; +// export type UpdateAccountConfigOptions = +// Partial & { +// environment?: Environment; +// }; export type PersonalAccessKeyOptions = { accountId: number; diff --git a/types/Config.ts b/types/Config.ts index ea24b094..0149849b 100644 --- a/types/Config.ts +++ b/types/Config.ts @@ -1,31 +1,38 @@ +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 defaultCmsPublishMode?: CmsPublishMode; httpTimeout?: number; env?: Environment; httpUseLocalhost?: 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 DeprecatedHubSpotConfigFields = { + portals?: Array; + defaultPortal?: number; + defaultMode?: CmsPublishMode; +}; -export type CLIConfig = CLIConfig_NEW | CLIConfig_DEPRECATED; +// 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 Environment = ValueOf | ''; @@ -44,3 +51,5 @@ export type GitInclusionResult = { configIgnored: boolean; gitignoreFiles: Array; }; + +export type ConfigFlag = ValueOf; From b2527214f0dfa4f52702ca8e0d48f8229aae0fdb Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Fri, 31 Jan 2025 15:50:02 -0500 Subject: [PATCH 02/70] finish getConfig --- config/config.ts | 5 ++-- config/configUtils.ts | 66 +++++++++++++++++++++++++++++++++++++++++++ lib/environment.ts | 2 +- types/Accounts.ts | 1 + 4 files changed, 70 insertions(+), 4 deletions(-) diff --git a/config/config.ts b/config/config.ts index 8fd86e24..3d942f3c 100644 --- a/config/config.ts +++ b/config/config.ts @@ -11,6 +11,7 @@ import { getGlobalConfigFilePath, readConfigFile, parseConfig, + loadConfigFromEnvironment, } from './configUtils'; export function getConfigFilePath(): string { @@ -41,7 +42,7 @@ export function getConfig( } if (useEnv) { - // do something else + return loadConfigFromEnvironment(); } const pathToRead = configFilePath || getConfigFilePath(); @@ -52,8 +53,6 @@ export function getConfig( return parseConfig(configFileSource); } -function getConfigIfExists(): HubSpotConfig | undefined {} - function isConfigValid(config: HubSpotConfig): boolean {} function createEmptyConfigFile(): void {} diff --git a/config/configUtils.ts b/config/configUtils.ts index 5b1b83d1..38fe2566 100644 --- a/config/configUtils.ts +++ b/config/configUtils.ts @@ -7,9 +7,18 @@ import { HUBSPOT_CONFIGURATION_FOLDER, HUBSPOT_CONFIGURATION_FILE, } from '../constants/config'; +import { ENVIRONMENT_VARIABLES } from '../constants/environments'; +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 } from '../types/Accounts'; +import { getValidEnv } from '../lib/environment'; export function getGlobalConfigFilePath(): string { return path.join( @@ -71,3 +80,60 @@ export function parseConfig(configSource: string): HubSpotConfig { return normalizeParsedConfig(parsedYaml); } + +export function loadConfigFromEnvironment(): 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]; + + if (!accountIdVar) { + throw new Error('@TODO'); + } + + const accountId = parseInt(accountIdVar); + const env = getValidEnv(hubspotEnvironment); + + let account: HubSpotConfigAccount; + + if (personalAccessKey) { + account = { + authType: PERSONAL_ACCESS_KEY_AUTH_METHOD.value, + accountId, + personalAccessKey, + env, + }; + } 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, + }; + } else if (apiKey) { + account = { + authType: API_KEY_AUTH_METHOD.value, + accountId, + apiKey, + env, + }; + } else { + throw new Error('@TODO'); + } + + return { accounts: [account] }; +} 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/types/Accounts.ts b/types/Accounts.ts index ab94d09e..9ce16517 100644 --- a/types/Accounts.ts +++ b/types/Accounts.ts @@ -16,6 +16,7 @@ export interface HubSpotConfigAccount { tokenInfo?: TokenInfo; clientId?: string; clientSecret?: string; + scopes?: Array; }; sandboxAccountType?: string | null; parentAccountId?: number | null; From 4595dc51bda1703d3b34d6aa6d0b91fff05d31c8 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Mon, 3 Feb 2025 14:40:04 -0500 Subject: [PATCH 03/70] more config WIP --- config/config.ts | 88 ++++++++++++++++++++++++++++++++++++++----- config/configUtils.ts | 79 ++++++++++++++++++++++++++++++++++++-- types/Accounts.ts | 4 +- 3 files changed, 156 insertions(+), 15 deletions(-) diff --git a/config/config.ts b/config/config.ts index 3d942f3c..7c2cefc8 100644 --- a/config/config.ts +++ b/config/config.ts @@ -1,4 +1,4 @@ -import fs from 'fs'; +import fs from 'fs-extra'; import findup from 'findup-sync'; import { DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME } from '../constants/config'; @@ -11,10 +11,11 @@ import { getGlobalConfigFilePath, readConfigFile, parseConfig, - loadConfigFromEnvironment, + buildConfigFromEnvironment, + writeConfigFile, } from './configUtils'; -export function getConfigFilePath(): string { +export function getDefaultConfigFilePath(): string { const globalConfigFilePath = getGlobalConfigFilePath(); if (fs.existsSync(globalConfigFilePath)) { @@ -34,18 +35,18 @@ export function getConfigFilePath(): string { } export function getConfig( - configFilePath?: string, - useEnv = false + configFilePath: string | null, + useEnv: boolean ): HubSpotConfig { if (configFilePath && useEnv) { throw new Error('@TODO'); } if (useEnv) { - return loadConfigFromEnvironment(); + return buildConfigFromEnvironment(); } - const pathToRead = configFilePath || getConfigFilePath(); + const pathToRead = configFilePath || getDefaultConfigFilePath(); logger.debug(`@TODOReading config from ${pathToRead}`); const configFileSource = readConfigFile(pathToRead); @@ -53,11 +54,78 @@ export function getConfig( return parseConfig(configFileSource); } -function isConfigValid(config: HubSpotConfig): boolean {} +export function isConfigValid( + configFilePath: string | null, + useEnv: boolean +): boolean { + const config = getConfig(configFilePath, useEnv); -function createEmptyConfigFile(): void {} + if (config.accounts.length === 0) { + logger.log('@TODO'); + return false; + } + + const accountIdsMap: { [key: number]: boolean } = {}; + const accountNamesMap: { [key: string]: boolean } = {}; + + return config.accounts.every(account => { + if (!account) { + logger.log('@TODO'); + return false; + } + if (!account.accountId) { + logger.log('@TODO'); + return false; + } + if (accountIdsMap[account.accountId]) { + logger.log('@TODO'); + return false; + } + if (account.name) { + if (accountNamesMap[account.name.toLowerCase()]) { + logger.log('@TODO'); + return false; + } + if (/\s+/.test(account.name)) { + logger.log('@TODO'); + return false; + } + accountNamesMap[account.name] = true; + } + + accountIdsMap[account.accountId] = true; + return true; + }); +} + +export function createEmptyConfigFile( + configFilePath: string, + useEnv: boolean +): void { + if (configFilePath && useEnv) { + throw new Error('@TODO'); + } else if (useEnv) { + return; + } + + const pathToWrite = configFilePath || getDefaultConfigFilePath(); -function deleteConfigFile(): void {} + writeConfigFile({ accounts: [] }, pathToWrite); +} + +export function deleteConfigFile( + configFilePath: string, + useEnv: boolean +): void { + if (configFilePath && useEnv) { + throw new Error('@TODO'); + } else if (useEnv) { + return; + } + + const pathToDelete = configFilePath || getDefaultConfigFilePath(); + fs.unlinkSync(pathToDelete); +} function getConfigAccountById(accountId: number): HubSpotConfigAccount {} diff --git a/config/configUtils.ts b/config/configUtils.ts index 38fe2566..aba298c8 100644 --- a/config/configUtils.ts +++ b/config/configUtils.ts @@ -1,11 +1,12 @@ import path from 'path'; import os from 'os'; -import fs from 'fs'; +import fs from 'fs-extra'; import yaml from 'js-yaml'; import { HUBSPOT_CONFIGURATION_FOLDER, HUBSPOT_CONFIGURATION_FILE, + HUBSPOT_ACCOUNT_TYPES, } from '../constants/config'; import { ENVIRONMENT_VARIABLES } from '../constants/environments'; import { @@ -17,7 +18,7 @@ import { import { HubSpotConfig, DeprecatedHubSpotConfigFields } from '../types/Config'; import { FileSystemError } from '../models/FileSystemError'; import { logger } from '../lib/logger'; -import { HubSpotConfigAccount } from '../types/Accounts'; +import { HubSpotConfigAccount, AccountType } from '../types/Accounts'; import { getValidEnv } from '../lib/environment'; export function getGlobalConfigFilePath(): string { @@ -47,6 +48,72 @@ export function readConfigFile(configPath: string): string { return source; } +// Ensure written config files have fields in a consistent order +function formatConfigForWrite(config: HubSpotConfig) { + const { + defaultAccount, + defaultCmsPublishMode, + httpTimeout, + allowUsageTracking, + accounts, + ...rest + } = config; + + return { + ...(defaultAccount && { defaultAccount }), + defaultCmsPublishMode, + httpTimeout, + allowUsageTracking, + ...rest, + accounts: accounts.map(account => { + const { name, accountId, env, authType, ...rest } = account; + + return { + name, + accountId, + env, + authType, + ...rest, + }; + }), + }; +} + +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); + logger.debug('@TODO'); + } 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 { @@ -65,6 +132,12 @@ export function normalizeParsedConfig( parsedConfig.defaultCmsPublishMode = parsedConfig.defaultMode; } + parsedConfig.accounts.forEach(account => { + if (!account.accountType) { + account.accountType = getAccountType(account.sandboxAccountType); + } + }); + return parsedConfig; } @@ -81,7 +154,7 @@ export function parseConfig(configSource: string): HubSpotConfig { return normalizeParsedConfig(parsedYaml); } -export function loadConfigFromEnvironment(): HubSpotConfig { +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]; diff --git a/types/Accounts.ts b/types/Accounts.ts index 9ce16517..e6c5a792 100644 --- a/types/Accounts.ts +++ b/types/Accounts.ts @@ -18,8 +18,8 @@ export interface HubSpotConfigAccount { clientSecret?: string; scopes?: Array; }; - sandboxAccountType?: string | null; - parentAccountId?: number | null; + sandboxAccountType?: string; + parentAccountId?: number; apiKey?: string; personalAccessKey?: string; } From 29d26ba2ee0bf72c2a1146c39c5ba150f4c62197 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Mon, 3 Feb 2025 16:25:18 -0500 Subject: [PATCH 04/70] more config WIP --- config/config.ts | 83 +++++++++++++++++++++++++++++++++++++------ config/configUtils.ts | 14 ++++++++ types/Config.ts | 4 +-- 3 files changed, 89 insertions(+), 12 deletions(-) diff --git a/config/config.ts b/config/config.ts index 7c2cefc8..2d3ebf6e 100644 --- a/config/config.ts +++ b/config/config.ts @@ -13,6 +13,8 @@ import { parseConfig, buildConfigFromEnvironment, writeConfigFile, + getLocalConfigFileDefaultPath, + getConfigAccountByIdentifier, } from './configUtils'; export function getDefaultConfigFilePath(): string { @@ -99,8 +101,9 @@ export function isConfigValid( } export function createEmptyConfigFile( - configFilePath: string, - useEnv: boolean + configFilePath: string | null, + useEnv: boolean, + useGlobalConfig = false ): void { if (configFilePath && useEnv) { throw new Error('@TODO'); @@ -108,13 +111,17 @@ export function createEmptyConfigFile( return; } - const pathToWrite = configFilePath || getDefaultConfigFilePath(); + const defaultPath = useGlobalConfig + ? getGlobalConfigFilePath() + : getLocalConfigFileDefaultPath(); + + const pathToWrite = configFilePath || defaultPath; writeConfigFile({ accounts: [] }, pathToWrite); } export function deleteConfigFile( - configFilePath: string, + configFilePath: string | null, useEnv: boolean ): void { if (configFilePath && useEnv) { @@ -127,17 +134,73 @@ export function deleteConfigFile( fs.unlinkSync(pathToDelete); } -function getConfigAccountById(accountId: number): HubSpotConfigAccount {} +export function getConfigAccountById( + configFilePath: string | null, + useEnv: boolean, + accountId: number +): HubSpotConfigAccount { + const { accounts } = getConfig(configFilePath, useEnv); + + const account = getConfigAccountByIdentifier( + accounts, + 'accountId', + accountId + ); + + if (!account) { + throw new Error('@TODO account not found'); + } -function getConfigAccountByName(accountName: string): HubSpotConfigAccount {} + return account; +} -function getConfigDefaultAccount(): HubSpotConfigAccount {} +export function getConfigAccountByName( + configFilePath: string | null, + useEnv: boolean, + accountName: string +): HubSpotConfigAccount { + const { accounts } = getConfig(configFilePath, useEnv); + + const account = getConfigAccountByIdentifier(accounts, 'name', accountName); -function getAllConfigAccounts(): HubSpotConfigAccount[]; + if (!account) { + throw new Error('@TODO account not found'); + } -function getAccountEnvironmentById(accountId: number): Environment {} + return account; +} -function getDefaultAccountEnvironment(): Environment {} +export function getConfigDefaultAccount( + configFilePath: string | null, + useEnv: boolean +): HubSpotConfigAccount { + const { accounts, defaultAccount } = getConfig(configFilePath, useEnv); + + if (!defaultAccount) { + throw new Error('@TODO no default account'); + } + + const account = getConfigAccountByIdentifier( + accounts, + 'name', + defaultAccount + ); + + if (!account) { + throw new Error('@TODO no default account'); + } + + return account; +} + +export function getAllConfigAccounts( + configFilePath: string | null, + useEnv: boolean +): HubSpotConfigAccount[] { + const { accounts } = getConfig(configFilePath, useEnv); + + return accounts; +} function updateConfigAccount( accoundId: number, diff --git a/config/configUtils.ts b/config/configUtils.ts index aba298c8..7eb9f07b 100644 --- a/config/configUtils.ts +++ b/config/configUtils.ts @@ -7,6 +7,7 @@ import { HUBSPOT_CONFIGURATION_FOLDER, HUBSPOT_CONFIGURATION_FILE, HUBSPOT_ACCOUNT_TYPES, + DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME, } from '../constants/config'; import { ENVIRONMENT_VARIABLES } from '../constants/environments'; import { @@ -20,6 +21,7 @@ import { FileSystemError } from '../models/FileSystemError'; import { logger } from '../lib/logger'; import { HubSpotConfigAccount, AccountType } from '../types/Accounts'; import { getValidEnv } from '../lib/environment'; +import { getCwd } from '../lib/path'; export function getGlobalConfigFilePath(): string { return path.join( @@ -29,6 +31,10 @@ export function getGlobalConfigFilePath(): string { ); } +export function getLocalConfigFileDefaultPath(): string { + return `${getCwd()}/${DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME}`; +} + export function readConfigFile(configPath: string): string { let source = ''; @@ -210,3 +216,11 @@ export function buildConfigFromEnvironment(): HubSpotConfig { return { accounts: [account] }; } + +export function getConfigAccountByIdentifier( + accounts: Array, + identifierFieldName: 'name' | 'accountId', + identifier: string | number +): HubSpotConfigAccount | undefined { + return accounts.find(account => account[identifierFieldName] === identifier); +} diff --git a/types/Config.ts b/types/Config.ts index 0149849b..95f13a71 100644 --- a/types/Config.ts +++ b/types/Config.ts @@ -10,7 +10,7 @@ import { ValueOf } from './Utils'; export interface HubSpotConfig { accounts: Array; allowUsageTracking?: boolean; - defaultAccount?: string | number; + defaultAccount?: string; defaultCmsPublishMode?: CmsPublishMode; httpTimeout?: number; env?: Environment; @@ -19,7 +19,7 @@ export interface HubSpotConfig { export type DeprecatedHubSpotConfigFields = { portals?: Array; - defaultPortal?: number; + defaultPortal?: string; defaultMode?: CmsPublishMode; }; From 63b55faa0ae5de9eabf8e4936c727d4cbd2df2df Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Tue, 4 Feb 2025 10:58:59 -0500 Subject: [PATCH 05/70] config wip --- config/config.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/config/config.ts b/config/config.ts index 2d3ebf6e..da09b9cf 100644 --- a/config/config.ts +++ b/config/config.ts @@ -2,7 +2,10 @@ import fs from 'fs-extra'; import findup from 'findup-sync'; import { DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME } from '../constants/config'; -import { HubSpotConfigAccount } from '../types/Accounts'; +import { + HubSpotConfigAccount, + HubSpotConfigAccountOptions, +} from '../types/Accounts'; import { HubSpotConfig, Environment, ConfigFlag } from '../types/Config'; import { CmsPublishMode } from '../types/Files'; import { logger } from '../lib/logger'; @@ -203,9 +206,15 @@ export function getAllConfigAccounts( } function updateConfigAccount( + configFilePath: string | null, + useEnv: boolean, accoundId: number, - fieldsToUpdate: object -): HubSpotConfigAccount {} + fieldsToUpdate: HubSpotConfigAccountOptions +): HubSpotConfigAccount { + if (useEnv) { + throw new Error('@TODO'); + } +} function setConfigAccountAsDefault(accountId: number): void {} From 9ab3c88faa65ce36ec175113d4585ea57cad975c Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Wed, 5 Feb 2025 11:02:06 -0500 Subject: [PATCH 06/70] -wip --- config/config.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/config/config.ts b/config/config.ts index da09b9cf..052c378f 100644 --- a/config/config.ts +++ b/config/config.ts @@ -179,6 +179,10 @@ export function getConfigDefaultAccount( ): HubSpotConfigAccount { const { accounts, defaultAccount } = getConfig(configFilePath, useEnv); + if (useEnv) { + return accounts[0]; + } + if (!defaultAccount) { throw new Error('@TODO no default account'); } @@ -205,7 +209,7 @@ export function getAllConfigAccounts( return accounts; } -function updateConfigAccount( +function addOrpdateConfigAccount( configFilePath: string | null, useEnv: boolean, accoundId: number, From 7404320eb8dce3114d4d0c2a4383fcab89b730c3 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Wed, 5 Feb 2025 14:44:23 -0500 Subject: [PATCH 07/70] update handling of env config --- config/config.ts | 22 +--------------------- config/configUtils.ts | 6 +++++- 2 files changed, 6 insertions(+), 22 deletions(-) diff --git a/config/config.ts b/config/config.ts index 052c378f..876ba76d 100644 --- a/config/config.ts +++ b/config/config.ts @@ -105,15 +105,8 @@ export function isConfigValid( export function createEmptyConfigFile( configFilePath: string | null, - useEnv: boolean, useGlobalConfig = false ): void { - if (configFilePath && useEnv) { - throw new Error('@TODO'); - } else if (useEnv) { - return; - } - const defaultPath = useGlobalConfig ? getGlobalConfigFilePath() : getLocalConfigFileDefaultPath(); @@ -123,16 +116,7 @@ export function createEmptyConfigFile( writeConfigFile({ accounts: [] }, pathToWrite); } -export function deleteConfigFile( - configFilePath: string | null, - useEnv: boolean -): void { - if (configFilePath && useEnv) { - throw new Error('@TODO'); - } else if (useEnv) { - return; - } - +export function deleteConfigFile(configFilePath: string | null): void { const pathToDelete = configFilePath || getDefaultConfigFilePath(); fs.unlinkSync(pathToDelete); } @@ -179,10 +163,6 @@ export function getConfigDefaultAccount( ): HubSpotConfigAccount { const { accounts, defaultAccount } = getConfig(configFilePath, useEnv); - if (useEnv) { - return accounts[0]; - } - if (!defaultAccount) { throw new Error('@TODO no default account'); } diff --git a/config/configUtils.ts b/config/configUtils.ts index 7eb9f07b..fb9f45f8 100644 --- a/config/configUtils.ts +++ b/config/configUtils.ts @@ -178,6 +178,7 @@ export function buildConfigFromEnvironment(): HubSpotConfig { } const accountId = parseInt(accountIdVar); + const env = getValidEnv(hubspotEnvironment); let account: HubSpotConfigAccount; @@ -188,6 +189,7 @@ export function buildConfigFromEnvironment(): HubSpotConfig { accountId, personalAccessKey, env, + name: accountIdVar, }; } else if (clientId && clientSecret && refreshToken) { account = { @@ -202,6 +204,7 @@ export function buildConfigFromEnvironment(): HubSpotConfig { }, }, env, + name: accountIdVar, }; } else if (apiKey) { account = { @@ -209,12 +212,13 @@ export function buildConfigFromEnvironment(): HubSpotConfig { accountId, apiKey, env, + name: accountIdVar, }; } else { throw new Error('@TODO'); } - return { accounts: [account] }; + return { accounts: [account], defaultAccount: accountIdVar }; } export function getConfigAccountByIdentifier( From 93c27be301312f7507fe4cd688e6fe88d5d4fc49 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Wed, 5 Feb 2025 18:12:21 -0500 Subject: [PATCH 08/70] Update accounts --- config/config.ts | 79 +++++++++++++++++++++++++++++++++++++------ config/configUtils.ts | 74 +++++++++++++++++++++++++++++++++++++++- types/Accounts.ts | 55 +++++++++++++----------------- 3 files changed, 166 insertions(+), 42 deletions(-) diff --git a/config/config.ts b/config/config.ts index 876ba76d..645d86ab 100644 --- a/config/config.ts +++ b/config/config.ts @@ -2,10 +2,7 @@ import fs from 'fs-extra'; import findup from 'findup-sync'; import { DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME } from '../constants/config'; -import { - HubSpotConfigAccount, - HubSpotConfigAccountOptions, -} from '../types/Accounts'; +import { HubSpotConfigAccount } from '../types/Accounts'; import { HubSpotConfig, Environment, ConfigFlag } from '../types/Config'; import { CmsPublishMode } from '../types/Files'; import { logger } from '../lib/logger'; @@ -18,6 +15,10 @@ import { writeConfigFile, getLocalConfigFileDefaultPath, getConfigAccountByIdentifier, + removeUndefinedFieldsFromConfigAccount, + hasAuthField, + isValidHubSpotConfigAccount, + getConfigAccountIndexById, } from './configUtils'; export function getDefaultConfigFilePath(): string { @@ -189,15 +190,73 @@ export function getAllConfigAccounts( return accounts; } -function addOrpdateConfigAccount( +// @TODO: Add logger debugs? +export function addConfigAccount( configFilePath: string | null, - useEnv: boolean, - accoundId: number, - fieldsToUpdate: HubSpotConfigAccountOptions -): HubSpotConfigAccount { - if (useEnv) { + accountToAdd: HubSpotConfigAccount +): void { + const config = getConfig(configFilePath, false); + const accountInConfig = getConfigAccountByIdentifier( + config.accounts, + 'accountId', + accountToAdd.accountId + ); + + if (accountInConfig) { + throw new Error('@TODO account already exists'); + } + + const configToWrite = removeUndefinedFieldsFromConfigAccount(accountToAdd); + + config.accounts.push(configToWrite); + + writeConfigFile(config, configFilePath || getDefaultConfigFilePath()); +} + +export function updateConfigAccount( + configFilePath: string | null, + accountId: number, + fieldsToUpdate: Partial +): void { + if (fieldsToUpdate.accountId !== accountId) { throw new Error('@TODO'); } + + const config = getConfig(configFilePath, false); + + const accountToUpdate = getConfigAccountByIdentifier( + config.accounts, + 'accountId', + accountId + ); + + if (!accountToUpdate) { + throw new Error('@TODO account not found'); + } + + const cleanedFieldsToUpdate = + removeUndefinedFieldsFromConfigAccount(fieldsToUpdate); + + const accountAuth = hasAuthField(accountToUpdate) ? accountToUpdate.auth : {}; + + const authFieldsToUpdate = hasAuthField(cleanedFieldsToUpdate) + ? { auth: { ...accountAuth, ...cleanedFieldsToUpdate.auth } } + : {}; + + const updatedAccount = { + ...accountToUpdate, + ...cleanedFieldsToUpdate, + ...authFieldsToUpdate, + }; + + if (!isValidHubSpotConfigAccount(updatedAccount)) { + throw new Error('@TODO'); + } + + const accountIndex = getConfigAccountIndexById(config.accounts, accountId); + config.accounts[accountIndex] = updatedAccount; + + writeConfigFile(config, configFilePath || getDefaultConfigFilePath()); } function setConfigAccountAsDefault(accountId: number): void {} diff --git a/config/configUtils.ts b/config/configUtils.ts index fb9f45f8..f739bb77 100644 --- a/config/configUtils.ts +++ b/config/configUtils.ts @@ -19,7 +19,11 @@ import { import { HubSpotConfig, DeprecatedHubSpotConfigFields } from '../types/Config'; import { FileSystemError } from '../models/FileSystemError'; import { logger } from '../lib/logger'; -import { HubSpotConfigAccount, AccountType } from '../types/Accounts'; +import { + HubSpotConfigAccount, + AccountType, + OAuthConfigAccount, +} from '../types/Accounts'; import { getValidEnv } from '../lib/environment'; import { getCwd } from '../lib/path'; @@ -161,6 +165,8 @@ export function parseConfig(configSource: string): HubSpotConfig { } export function buildConfigFromEnvironment(): HubSpotConfig { + // @TODO: Add other config fields + // @TODO: handle account type? 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]; @@ -228,3 +234,69 @@ export function getConfigAccountByIdentifier( ): HubSpotConfigAccount | undefined { return accounts.find(account => account[identifierFieldName] === identifier); } + +export function getConfigAccountIndexById( + accounts: Array, + id: string | number +): number { + return accounts.findIndex(account => account.accountId === id); +} + +export function hasAuthField( + account: Partial +): account is OAuthConfigAccount { + return 'auth' in account && typeof account.auth === 'object'; +} + +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 (hasAuthField(account)) { + Object.keys(account.auth).forEach(k => { + const key = k as keyof T; + if (account[key] === undefined) { + delete account[key]; + } + }); + } + + return account; +} + +export function isValidHubSpotConfigAccount( + account: unknown +): account is HubSpotConfigAccount { + if (!account || typeof account !== 'object') { + return false; + } + + if (!('authType' in account) || typeof account.authType !== 'string') { + return false; + } + + if (account.authType === PERSONAL_ACCESS_KEY_AUTH_METHOD.value) { + return ( + 'personalAccessKey' in account && + typeof account.personalAccessKey === 'string' + ); + } + + if (account.authType === OAUTH_AUTH_METHOD.value) { + return 'auth' in account && typeof account.auth === 'object'; + } + + if (account.authType === API_KEY_AUTH_METHOD.value) { + return 'apiKey' in account && typeof account.apiKey === 'string'; + } + + return false; +} diff --git a/types/Accounts.ts b/types/Accounts.ts index e6c5a792..c2b2eeac 100644 --- a/types/Accounts.ts +++ b/types/Accounts.ts @@ -2,26 +2,22 @@ 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 HubSpotConfigAccount { - name?: string; +interface BaseHubSpotConfigAccount { + name: string; accountId: number; - accountType?: AccountType; + accountType?: AccountType; // @TODO: make required? defaultCmsPublishMode?: CmsPublishMode; env: Environment; - authType?: AuthType; - auth?: { - tokenInfo?: TokenInfo; - clientId?: string; - clientSecret?: string; - scopes?: Array; - }; + authType: AuthType; sandboxAccountType?: string; parentAccountId?: number; - apiKey?: string; - personalAccessKey?: string; } export type DeprecatedHubSpotConfigAccountFields = { @@ -41,34 +37,31 @@ export type TokenInfo = { refreshToken?: string; }; -export interface PersonalAccessKeyConfigAccount extends HubSpotConfigAccount { - authType: 'personalaccesskey'; +export interface PersonalAccessKeyConfigAccount + extends BaseHubSpotConfigAccount { + authType: typeof PERSONAL_ACCESS_KEY_AUTH_METHOD.value; personalAccessKey: string; } -export interface OAuthConfigAccount extends HubSpotConfigAccount { - 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 interface APIKeyConfigAccount extends HubSpotConfigAccount { - authType: 'apikey'; +export interface APIKeyConfigAccount extends BaseHubSpotConfigAccount { + authType: typeof API_KEY_AUTH_METHOD.value; apiKey: string; } -export interface HubSpotConfigAccountOptions extends HubSpotConfigAccount { - tokenInfo?: TokenInfo; - clientId?: string; - clientSecret?: string; - scopes?: Array; - apiKey?: string; - personalAccessKey?: string; -} +export type HubSpotConfigAccount = + | PersonalAccessKeyConfigAccount + | OAuthConfigAccount + | APIKeyConfigAccount; export type ScopeData = { portalScopesInGroup: Array; From d521932cf58669b1cf745296a0b422f31aa1a884 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Thu, 6 Feb 2025 12:59:52 -0500 Subject: [PATCH 09/70] config account adding/updating --- config/config.ts | 50 +++++++++----------------------- config/configUtils.ts | 67 +++++++++++++++++++++---------------------- 2 files changed, 45 insertions(+), 72 deletions(-) diff --git a/config/config.ts b/config/config.ts index 645d86ab..b9115372 100644 --- a/config/config.ts +++ b/config/config.ts @@ -15,9 +15,8 @@ import { writeConfigFile, getLocalConfigFileDefaultPath, getConfigAccountByIdentifier, - removeUndefinedFieldsFromConfigAccount, hasAuthField, - isValidHubSpotConfigAccount, + isConfigAccountValid, getConfigAccountIndexById, } from './configUtils'; @@ -75,11 +74,7 @@ export function isConfigValid( const accountNamesMap: { [key: string]: boolean } = {}; return config.accounts.every(account => { - if (!account) { - logger.log('@TODO'); - return false; - } - if (!account.accountId) { + if (!isConfigAccountValid(account)) { logger.log('@TODO'); return false; } @@ -195,7 +190,12 @@ export function addConfigAccount( configFilePath: string | null, accountToAdd: HubSpotConfigAccount ): void { + if (!isConfigAccountValid(accountToAdd)) { + throw new Error('@TODO'); + } + const config = getConfig(configFilePath, false); + const accountInConfig = getConfigAccountByIdentifier( config.accounts, 'accountId', @@ -206,54 +206,30 @@ export function addConfigAccount( throw new Error('@TODO account already exists'); } - const configToWrite = removeUndefinedFieldsFromConfigAccount(accountToAdd); - - config.accounts.push(configToWrite); + config.accounts.push(accountToAdd); writeConfigFile(config, configFilePath || getDefaultConfigFilePath()); } export function updateConfigAccount( configFilePath: string | null, - accountId: number, - fieldsToUpdate: Partial + updatedAccount: HubSpotConfigAccount ): void { - if (fieldsToUpdate.accountId !== accountId) { + if (!isConfigAccountValid(updatedAccount)) { throw new Error('@TODO'); } const config = getConfig(configFilePath, false); - const accountToUpdate = getConfigAccountByIdentifier( + const accountIndex = getConfigAccountIndexById( config.accounts, - 'accountId', - accountId + updatedAccount.accountId ); - if (!accountToUpdate) { + if (accountIndex < 0) { throw new Error('@TODO account not found'); } - const cleanedFieldsToUpdate = - removeUndefinedFieldsFromConfigAccount(fieldsToUpdate); - - const accountAuth = hasAuthField(accountToUpdate) ? accountToUpdate.auth : {}; - - const authFieldsToUpdate = hasAuthField(cleanedFieldsToUpdate) - ? { auth: { ...accountAuth, ...cleanedFieldsToUpdate.auth } } - : {}; - - const updatedAccount = { - ...accountToUpdate, - ...cleanedFieldsToUpdate, - ...authFieldsToUpdate, - }; - - if (!isValidHubSpotConfigAccount(updatedAccount)) { - throw new Error('@TODO'); - } - - const accountIndex = getConfigAccountIndexById(config.accounts, accountId); config.accounts[accountIndex] = updatedAccount; writeConfigFile(config, configFilePath || getDefaultConfigFilePath()); diff --git a/config/configUtils.ts b/config/configUtils.ts index f739bb77..0620424f 100644 --- a/config/configUtils.ts +++ b/config/configUtils.ts @@ -58,6 +58,30 @@ export function readConfigFile(configPath: string): string { 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 (hasAuthField(account)) { + Object.keys(account.auth).forEach(k => { + const key = k as keyof T; + if (account[key] === undefined) { + delete account[key]; + } + }); + } + + return account; +} + // Ensure written config files have fields in a consistent order function formatConfigForWrite(config: HubSpotConfig) { const { @@ -69,7 +93,7 @@ function formatConfigForWrite(config: HubSpotConfig) { ...rest } = config; - return { + const orderedConfig = { ...(defaultAccount && { defaultAccount }), defaultCmsPublishMode, httpTimeout, @@ -87,6 +111,8 @@ function formatConfigForWrite(config: HubSpotConfig) { }; }), }; + + return removeUndefinedFieldsFromConfigAccount(orderedConfig); } export function writeConfigFile( @@ -248,54 +274,25 @@ export function hasAuthField( return 'auth' in account && typeof account.auth === 'object'; } -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 (hasAuthField(account)) { - Object.keys(account.auth).forEach(k => { - const key = k as keyof T; - if (account[key] === undefined) { - delete account[key]; - } - }); - } - - return account; -} - -export function isValidHubSpotConfigAccount( - account: unknown -): account is HubSpotConfigAccount { +export function isConfigAccountValid(account: HubSpotConfigAccount) { if (!account || typeof account !== 'object') { return false; } - if (!('authType' in account) || typeof account.authType !== 'string') { + if (!account.authType) { return false; } if (account.authType === PERSONAL_ACCESS_KEY_AUTH_METHOD.value) { - return ( - 'personalAccessKey' in account && - typeof account.personalAccessKey === 'string' - ); + return 'personalAccessKey' in account && account.personalAccessKey; } if (account.authType === OAUTH_AUTH_METHOD.value) { - return 'auth' in account && typeof account.auth === 'object'; + return 'auth' in account && account.auth; } if (account.authType === API_KEY_AUTH_METHOD.value) { - return 'apiKey' in account && typeof account.apiKey === 'string'; + return 'apiKey' in account && account.apiKey; } return false; From fc6ca45171690692689499953f3af2fabb15cafc Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Thu, 6 Feb 2025 16:33:49 -0500 Subject: [PATCH 10/70] finish config exportable first pass --- config/config.ts | 148 ++++++++++++++++++++++++++++++++++++++---- config/configUtils.ts | 14 +--- types/Config.ts | 3 +- 3 files changed, 139 insertions(+), 26 deletions(-) diff --git a/config/config.ts b/config/config.ts index b9115372..0145a021 100644 --- a/config/config.ts +++ b/config/config.ts @@ -1,12 +1,14 @@ import fs from 'fs-extra'; import findup from 'findup-sync'; -import { DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME } from '../constants/config'; +import { + DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME, + MIN_HTTP_TIMEOUT, +} from '../constants/config'; import { HubSpotConfigAccount } from '../types/Accounts'; -import { HubSpotConfig, Environment, ConfigFlag } from '../types/Config'; +import { HubSpotConfig, ConfigFlag } from '../types/Config'; import { CmsPublishMode } from '../types/Files'; import { logger } from '../lib/logger'; -import { i18n } from '../utils/lang'; import { getGlobalConfigFilePath, readConfigFile, @@ -15,10 +17,10 @@ import { writeConfigFile, getLocalConfigFileDefaultPath, getConfigAccountByIdentifier, - hasAuthField, isConfigAccountValid, getConfigAccountIndexById, } from './configUtils'; +import { CMS_PUBLISH_MODE } from '../constants/files'; export function getDefaultConfigFilePath(): string { const globalConfigFilePath = getGlobalConfigFilePath(); @@ -165,7 +167,7 @@ export function getConfigDefaultAccount( const account = getConfigAccountByIdentifier( accounts, - 'name', + 'accountId', defaultAccount ); @@ -235,18 +237,138 @@ export function updateConfigAccount( writeConfigFile(config, configFilePath || getDefaultConfigFilePath()); } -function setConfigAccountAsDefault(accountId: number): void {} +export function setConfigAccountAsDefault( + configFilePath: string | null, + accountIdentifier: number | string +): void { + const config = getConfig(configFilePath, false); + + const identifierAsNumber = + typeof accountIdentifier === 'number' + ? accountIdentifier + : parseInt(accountIdentifier); + const isId = !isNaN(identifierAsNumber); + + const account = getConfigAccountByIdentifier( + config.accounts, + isId ? 'accountId' : 'name', + isId ? identifierAsNumber : accountIdentifier + ); + + if (!account) { + throw new Error('@TODO account not found'); + } + + config.defaultAccount = account.accountId; + writeConfigFile(config, configFilePath || getDefaultConfigFilePath()); +} + +export function renameConfigAccount( + configFilePath: string | null, + currentName: string, + newName: string +): void { + const config = getConfig(configFilePath, false); + + const account = getConfigAccountByIdentifier( + config.accounts, + 'name', + currentName + ); -function renameConfigAccount(accountId: number, newName: string): void {} + if (!account) { + throw new Error('@TODO account not found'); + } -function removeAccountFromConfig(accountId: number): void {} + const duplicateAccount = getConfigAccountByIdentifier( + config.accounts, + 'name', + newName + ); + + if (duplicateAccount) { + throw new Error('@TODO account name already exists'); + } -function updateHttpTimeout(timeout: number): void {} + account.name = newName; -function updateAllowUsageTracking(isAllowed: boolean): void {} + writeConfigFile(config, configFilePath || getDefaultConfigFilePath()); +} -function updateDefaultCmsPublishMode(cmsPublishMode: CmsPublishMode): void {} +export function removeAccountFromConfig( + configFilePath: string | null, + accountId: number +): void { + const config = getConfig(configFilePath, false); -function isConfigFlagEnabled(flag: ConfigFlag): boolean {} + const index = getConfigAccountIndexById(config.accounts, accountId); -function isUsageTrackingAllowed(): boolean {} + if (index < 0) { + throw new Error('@TODO account does not exist'); + } + + config.accounts.splice(index, 1); + + writeConfigFile(config, configFilePath || getDefaultConfigFilePath()); +} + +export function updateHttpTimeout( + configFilePath: string | null, + timeout: string | number +): void { + const parsedTimeout = + typeof timeout === 'string' ? parseInt(timeout) : timeout; + + if (isNaN(parsedTimeout) || parsedTimeout < MIN_HTTP_TIMEOUT) { + throw new Error('@TODO timeout must be greater than min'); + } + + const config = getConfig(configFilePath, false); + + config.httpTimeout = parsedTimeout; + + writeConfigFile(config, configFilePath || getDefaultConfigFilePath()); +} + +export function updateAllowUsageTracking( + configFilePath: string | null, + isAllowed: boolean +): void { + const config = getConfig(configFilePath, false); + + config.allowUsageTracking = isAllowed; + + writeConfigFile(config, configFilePath || getDefaultConfigFilePath()); +} + +export function updateDefaultCmsPublishMode( + configFilePath: string | null, + cmsPublishMode: CmsPublishMode +): void { + if ( + !cmsPublishMode || + !Object.values(CMS_PUBLISH_MODE).includes(cmsPublishMode) + ) { + throw new Error('@TODO invalid CMS publihs mode'); + } + + const config = getConfig(configFilePath, false); + + config.defaultCmsPublishMode = cmsPublishMode; + + writeConfigFile(config, configFilePath || getDefaultConfigFilePath()); +} + +export function isConfigFlagEnabled( + configFilePath: string | null, + flag: ConfigFlag, + defaultValue: boolean +): boolean { + const config = getConfig(configFilePath, false); + + if (typeof config[flag] === 'undefined') { + return defaultValue; + } + + return Boolean(config[flag]); +} diff --git a/config/configUtils.ts b/config/configUtils.ts index 0620424f..dfa8060a 100644 --- a/config/configUtils.ts +++ b/config/configUtils.ts @@ -19,11 +19,7 @@ import { import { HubSpotConfig, DeprecatedHubSpotConfigFields } from '../types/Config'; import { FileSystemError } from '../models/FileSystemError'; import { logger } from '../lib/logger'; -import { - HubSpotConfigAccount, - AccountType, - OAuthConfigAccount, -} from '../types/Accounts'; +import { HubSpotConfigAccount, AccountType } from '../types/Accounts'; import { getValidEnv } from '../lib/environment'; import { getCwd } from '../lib/path'; @@ -70,7 +66,7 @@ export function removeUndefinedFieldsFromConfigAccount< } }); - if (hasAuthField(account)) { + if ('auth' in account && typeof account.auth === 'object') { Object.keys(account.auth).forEach(k => { const key = k as keyof T; if (account[key] === undefined) { @@ -268,12 +264,6 @@ export function getConfigAccountIndexById( return accounts.findIndex(account => account.accountId === id); } -export function hasAuthField( - account: Partial -): account is OAuthConfigAccount { - return 'auth' in account && typeof account.auth === 'object'; -} - export function isConfigAccountValid(account: HubSpotConfigAccount) { if (!account || typeof account !== 'object') { return false; diff --git a/types/Config.ts b/types/Config.ts index 95f13a71..b4bfaa0f 100644 --- a/types/Config.ts +++ b/types/Config.ts @@ -10,11 +10,12 @@ import { ValueOf } from './Utils'; export interface HubSpotConfig { accounts: Array; allowUsageTracking?: boolean; - defaultAccount?: string; + defaultAccount?: number; defaultCmsPublishMode?: CmsPublishMode; httpTimeout?: number; env?: Environment; httpUseLocalhost?: boolean; + useCustomObjectHubfile?: boolean; } export type DeprecatedHubSpotConfigFields = { From c7156023d0d346dfb439578b4235fe89c72aa156 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Thu, 6 Feb 2025 17:02:24 -0500 Subject: [PATCH 11/70] add back missing methods we might need --- config/config.ts | 38 +++++++++++++++++++++++++++----------- config/configUtils.ts | 8 ++++++++ 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/config/config.ts b/config/config.ts index 0145a021..c3ac8f03 100644 --- a/config/config.ts +++ b/config/config.ts @@ -1,16 +1,13 @@ import fs from 'fs-extra'; -import findup from 'findup-sync'; -import { - DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME, - MIN_HTTP_TIMEOUT, -} from '../constants/config'; +import { 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, @@ -21,6 +18,15 @@ import { getConfigAccountIndexById, } from './configUtils'; import { CMS_PUBLISH_MODE } from '../constants/files'; +import { Environment } from '../types/Config'; + +export function localConfigFileExists(): boolean { + return Boolean(getLocalConfigFilePath()); +} + +export function globalConfigFileExists(): boolean { + return fs.existsSync(getGlobalConfigFilePath()); +} export function getDefaultConfigFilePath(): string { const globalConfigFilePath = getGlobalConfigFilePath(); @@ -29,16 +35,13 @@ export function getDefaultConfigFilePath(): string { return globalConfigFilePath; } - const localConfigPath = findup([ - DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME, - DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME.replace('.yml', '.yaml'), - ]); + const localConfigFilePath = getLocalConfigFilePath(); - if (!localConfigPath) { + if (!localConfigFilePath) { throw new Error('@TODO'); } - return localConfigPath; + return localConfigFilePath; } export function getConfig( @@ -187,6 +190,19 @@ export function getAllConfigAccounts( return accounts; } +export function getConfigAccountEnvironment( + configFilePath: string | null, + useEnv: boolean, + accountId?: number +): Environment { + if (accountId) { + const account = getConfigAccountById(configFilePath, useEnv, accountId); + return account.env; + } + const defaultAccount = getConfigDefaultAccount(configFilePath, useEnv); + return defaultAccount.env; +} + // @TODO: Add logger debugs? export function addConfigAccount( configFilePath: string | null, diff --git a/config/configUtils.ts b/config/configUtils.ts index dfa8060a..d83b518e 100644 --- a/config/configUtils.ts +++ b/config/configUtils.ts @@ -2,6 +2,7 @@ import path from 'path'; import os from 'os'; import fs from 'fs-extra'; import yaml from 'js-yaml'; +import findup from 'findup-sync'; import { HUBSPOT_CONFIGURATION_FOLDER, @@ -31,6 +32,13 @@ export function getGlobalConfigFilePath(): string { ); } +export function getLocalConfigFilePath(): string | null { + return findup([ + DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME, + DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME.replace('.yml', '.yaml'), + ]); +} + export function getLocalConfigFileDefaultPath(): string { return `${getCwd()}/${DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME}`; } From 06669df635223751eaeb7c7aba4aad8b6beffeba Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Fri, 7 Feb 2025 15:06:29 -0500 Subject: [PATCH 12/70] handle new environment variables --- config/configUtils.ts | 32 +++++++++++++++++++++++++++++--- constants/environments.ts | 5 +++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/config/configUtils.ts b/config/configUtils.ts index d83b518e..b236143c 100644 --- a/config/configUtils.ts +++ b/config/configUtils.ts @@ -23,6 +23,7 @@ import { logger } from '../lib/logger'; import { HubSpotConfigAccount, AccountType } from '../types/Accounts'; import { getValidEnv } from '../lib/environment'; import { getCwd } from '../lib/path'; +import { CMS_PUBLISH_MODE } from '../constants/files'; export function getGlobalConfigFilePath(): string { return path.join( @@ -165,7 +166,7 @@ export function normalizeParsedConfig( } if (parsedConfig.defaultPortal) { - parsedConfig.defaultAccount = parsedConfig.defaultPortal; + parsedConfig.defaultAccount = parseInt(parsedConfig.defaultPortal); } if (parsedConfig.defaultMode) { @@ -195,7 +196,6 @@ export function parseConfig(configSource: string): HubSpotConfig { } export function buildConfigFromEnvironment(): HubSpotConfig { - // @TODO: Add other config fields // @TODO: handle account type? const apiKey = process.env[ENVIRONMENT_VARIABLES.HUBSPOT_API_KEY]; const clientId = process.env[ENVIRONMENT_VARIABLES.HUBSPOT_CLIENT_ID]; @@ -208,12 +208,31 @@ export function buildConfigFromEnvironment(): HubSpotConfig { 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('@TODO'); } 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); @@ -254,7 +273,14 @@ export function buildConfigFromEnvironment(): HubSpotConfig { throw new Error('@TODO'); } - return { accounts: [account], defaultAccount: accountIdVar }; + return { + accounts: [account], + defaultAccount: accountId, + httpTimeout, + httpUseLocalhost, + allowUsageTracking, + defaultCmsPublishMode, + }; } export function getConfigAccountByIdentifier( diff --git a/constants/environments.ts b/constants/environments.ts index cb704fa2..a6c5c449 100644 --- a/constants/environments.ts +++ b/constants/environments.ts @@ -13,4 +13,9 @@ export const ENVIRONMENT_VARIABLES = { 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_CONFIG: 'USE_ENVIRONMENT_CONFIG', + HUBSPOT_CONFIG_PATH: 'HUBSPOT_CONFIG_PATH', } as const; From 24922895c57159adfe5c0e414d379663f62dc97a Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Fri, 7 Feb 2025 15:45:45 -0500 Subject: [PATCH 13/70] Add config source options to environment --- config/config.ts | 136 ++++++++++++++++-------------------------- config/configUtils.ts | 19 ++++++ 2 files changed, 69 insertions(+), 86 deletions(-) diff --git a/config/config.ts b/config/config.ts index c3ac8f03..a4109bec 100644 --- a/config/config.ts +++ b/config/config.ts @@ -16,6 +16,7 @@ import { getConfigAccountByIdentifier, isConfigAccountValid, getConfigAccountIndexById, + getConfigPathEnvironmentVariables, } from './configUtils'; import { CMS_PUBLISH_MODE } from '../constants/files'; import { Environment } from '../types/Config'; @@ -28,7 +29,7 @@ export function globalConfigFileExists(): boolean { return fs.existsSync(getGlobalConfigFilePath()); } -export function getDefaultConfigFilePath(): string { +function getDefaultConfigFilePath(): string { const globalConfigFilePath = getGlobalConfigFilePath(); if (fs.existsSync(globalConfigFilePath)) { @@ -44,19 +45,20 @@ export function getDefaultConfigFilePath(): string { return localConfigFilePath; } -export function getConfig( - configFilePath: string | null, - useEnv: boolean -): HubSpotConfig { - if (configFilePath && useEnv) { - throw new Error('@TODO'); - } +export function getConfigFilePath(): string { + const { configFilePathFromEnvironment } = getConfigPathEnvironmentVariables(); + + return configFilePathFromEnvironment || getDefaultConfigFilePath(); +} - if (useEnv) { +export function getConfig(): HubSpotConfig { + const { useEnvironmentConfig } = getConfigPathEnvironmentVariables(); + + if (useEnvironmentConfig) { return buildConfigFromEnvironment(); } - const pathToRead = configFilePath || getDefaultConfigFilePath(); + const pathToRead = getConfigFilePath(); logger.debug(`@TODOReading config from ${pathToRead}`); const configFileSource = readConfigFile(pathToRead); @@ -64,11 +66,8 @@ export function getConfig( return parseConfig(configFileSource); } -export function isConfigValid( - configFilePath: string | null, - useEnv: boolean -): boolean { - const config = getConfig(configFilePath, useEnv); +export function isConfigValid(): boolean { + const config = getConfig(); if (config.accounts.length === 0) { logger.log('@TODO'); @@ -104,30 +103,24 @@ export function isConfigValid( }); } -export function createEmptyConfigFile( - configFilePath: string | null, - useGlobalConfig = false -): void { +export function createEmptyConfigFile(useGlobalConfig = false): void { + const { configFilePathFromEnvironment } = getConfigPathEnvironmentVariables(); const defaultPath = useGlobalConfig ? getGlobalConfigFilePath() : getLocalConfigFileDefaultPath(); - const pathToWrite = configFilePath || defaultPath; + const pathToWrite = configFilePathFromEnvironment || defaultPath; writeConfigFile({ accounts: [] }, pathToWrite); } -export function deleteConfigFile(configFilePath: string | null): void { - const pathToDelete = configFilePath || getDefaultConfigFilePath(); +export function deleteConfigFile(): void { + const pathToDelete = getConfigFilePath(); fs.unlinkSync(pathToDelete); } -export function getConfigAccountById( - configFilePath: string | null, - useEnv: boolean, - accountId: number -): HubSpotConfigAccount { - const { accounts } = getConfig(configFilePath, useEnv); +export function getConfigAccountById(accountId: number): HubSpotConfigAccount { + const { accounts } = getConfig(); const account = getConfigAccountByIdentifier( accounts, @@ -143,11 +136,9 @@ export function getConfigAccountById( } export function getConfigAccountByName( - configFilePath: string | null, - useEnv: boolean, accountName: string ): HubSpotConfigAccount { - const { accounts } = getConfig(configFilePath, useEnv); + const { accounts } = getConfig(); const account = getConfigAccountByIdentifier(accounts, 'name', accountName); @@ -158,11 +149,8 @@ export function getConfigAccountByName( return account; } -export function getConfigDefaultAccount( - configFilePath: string | null, - useEnv: boolean -): HubSpotConfigAccount { - const { accounts, defaultAccount } = getConfig(configFilePath, useEnv); +export function getConfigDefaultAccount(): HubSpotConfigAccount { + const { accounts, defaultAccount } = getConfig(); if (!defaultAccount) { throw new Error('@TODO no default account'); @@ -181,38 +169,28 @@ export function getConfigDefaultAccount( return account; } -export function getAllConfigAccounts( - configFilePath: string | null, - useEnv: boolean -): HubSpotConfigAccount[] { - const { accounts } = getConfig(configFilePath, useEnv); +export function getAllConfigAccounts(): HubSpotConfigAccount[] { + const { accounts } = getConfig(); return accounts; } -export function getConfigAccountEnvironment( - configFilePath: string | null, - useEnv: boolean, - accountId?: number -): Environment { +export function getConfigAccountEnvironment(accountId?: number): Environment { if (accountId) { - const account = getConfigAccountById(configFilePath, useEnv, accountId); + const account = getConfigAccountById(accountId); return account.env; } - const defaultAccount = getConfigDefaultAccount(configFilePath, useEnv); + const defaultAccount = getConfigDefaultAccount(); return defaultAccount.env; } // @TODO: Add logger debugs? -export function addConfigAccount( - configFilePath: string | null, - accountToAdd: HubSpotConfigAccount -): void { +export function addConfigAccount(accountToAdd: HubSpotConfigAccount): void { if (!isConfigAccountValid(accountToAdd)) { throw new Error('@TODO'); } - const config = getConfig(configFilePath, false); + const config = getConfig(); const accountInConfig = getConfigAccountByIdentifier( config.accounts, @@ -226,18 +204,17 @@ export function addConfigAccount( config.accounts.push(accountToAdd); - writeConfigFile(config, configFilePath || getDefaultConfigFilePath()); + writeConfigFile(config, getConfigFilePath()); } export function updateConfigAccount( - configFilePath: string | null, updatedAccount: HubSpotConfigAccount ): void { if (!isConfigAccountValid(updatedAccount)) { throw new Error('@TODO'); } - const config = getConfig(configFilePath, false); + const config = getConfig(); const accountIndex = getConfigAccountIndexById( config.accounts, @@ -250,14 +227,13 @@ export function updateConfigAccount( config.accounts[accountIndex] = updatedAccount; - writeConfigFile(config, configFilePath || getDefaultConfigFilePath()); + writeConfigFile(config, getConfigFilePath()); } export function setConfigAccountAsDefault( - configFilePath: string | null, accountIdentifier: number | string ): void { - const config = getConfig(configFilePath, false); + const config = getConfig(); const identifierAsNumber = typeof accountIdentifier === 'number' @@ -276,15 +252,14 @@ export function setConfigAccountAsDefault( } config.defaultAccount = account.accountId; - writeConfigFile(config, configFilePath || getDefaultConfigFilePath()); + writeConfigFile(config, getConfigFilePath()); } export function renameConfigAccount( - configFilePath: string | null, currentName: string, newName: string ): void { - const config = getConfig(configFilePath, false); + const config = getConfig(); const account = getConfigAccountByIdentifier( config.accounts, @@ -308,14 +283,11 @@ export function renameConfigAccount( account.name = newName; - writeConfigFile(config, configFilePath || getDefaultConfigFilePath()); + writeConfigFile(config, getConfigFilePath()); } -export function removeAccountFromConfig( - configFilePath: string | null, - accountId: number -): void { - const config = getConfig(configFilePath, false); +export function removeAccountFromConfig(accountId: number): void { + const config = getConfig(); const index = getConfigAccountIndexById(config.accounts, accountId); @@ -325,13 +297,10 @@ export function removeAccountFromConfig( config.accounts.splice(index, 1); - writeConfigFile(config, configFilePath || getDefaultConfigFilePath()); + writeConfigFile(config, getConfigFilePath()); } -export function updateHttpTimeout( - configFilePath: string | null, - timeout: string | number -): void { +export function updateHttpTimeout(timeout: string | number): void { const parsedTimeout = typeof timeout === 'string' ? parseInt(timeout) : timeout; @@ -339,26 +308,22 @@ export function updateHttpTimeout( throw new Error('@TODO timeout must be greater than min'); } - const config = getConfig(configFilePath, false); + const config = getConfig(); config.httpTimeout = parsedTimeout; - writeConfigFile(config, configFilePath || getDefaultConfigFilePath()); + writeConfigFile(config, getConfigFilePath()); } -export function updateAllowUsageTracking( - configFilePath: string | null, - isAllowed: boolean -): void { - const config = getConfig(configFilePath, false); +export function updateAllowUsageTracking(isAllowed: boolean): void { + const config = getConfig(); config.allowUsageTracking = isAllowed; - writeConfigFile(config, configFilePath || getDefaultConfigFilePath()); + writeConfigFile(config, getConfigFilePath()); } export function updateDefaultCmsPublishMode( - configFilePath: string | null, cmsPublishMode: CmsPublishMode ): void { if ( @@ -368,19 +333,18 @@ export function updateDefaultCmsPublishMode( throw new Error('@TODO invalid CMS publihs mode'); } - const config = getConfig(configFilePath, false); + const config = getConfig(); config.defaultCmsPublishMode = cmsPublishMode; - writeConfigFile(config, configFilePath || getDefaultConfigFilePath()); + writeConfigFile(config, getConfigFilePath()); } export function isConfigFlagEnabled( - configFilePath: string | null, flag: ConfigFlag, defaultValue: boolean ): boolean { - const config = getConfig(configFilePath, false); + const config = getConfig(); if (typeof config[flag] === 'undefined') { return defaultValue; diff --git a/config/configUtils.ts b/config/configUtils.ts index b236143c..7758b6a5 100644 --- a/config/configUtils.ts +++ b/config/configUtils.ts @@ -44,6 +44,25 @@ export function getLocalConfigFileDefaultPath(): 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_CONFIG] === 'true'; + + if (configFilePathFromEnvironment && useEnvironmentConfig) { + throw new Error('@TODO'); + } + + return { + configFilePathFromEnvironment, + useEnvironmentConfig, + }; +} + export function readConfigFile(configPath: string): string { let source = ''; From 258be5e87ade094a4e218af1fdbf48faf37c67f0 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Fri, 7 Feb 2025 17:32:46 -0500 Subject: [PATCH 14/70] Cleanup wip --- config/CLIConfiguration.ts | 644 ------------------- config/README.md | 2 + config/__tests__/environment.test.ts | 2 + config/config.ts | 354 ----------- config/configFile.ts | 120 ---- config/configUtils_OLD.ts | 139 ---- config/config_DEPRECATED.ts | 918 --------------------------- config/environment.ts | 81 --- config/getAccountIdentifier.ts | 13 - config/index.ts | 490 ++++++++------ config/{configUtils.ts => utils.ts} | 15 + http/getAxiosConfig.ts | 4 +- http/index.ts | 44 +- lib/cms/themes.ts | 6 +- lib/oauth.ts | 35 +- lib/personalAccessKey.ts | 104 ++- lib/trackUsage.ts | 11 +- models/OAuth2Manager.ts | 63 +- types/Accounts.ts | 25 +- types/Config.ts | 21 - 20 files changed, 434 insertions(+), 2657 deletions(-) delete mode 100644 config/CLIConfiguration.ts delete mode 100644 config/config.ts delete mode 100644 config/configFile.ts delete mode 100644 config/configUtils_OLD.ts delete mode 100644 config/config_DEPRECATED.ts delete mode 100644 config/environment.ts delete mode 100644 config/getAccountIdentifier.ts rename config/{configUtils.ts => utils.ts} (95%) diff --git a/config/CLIConfiguration.ts b/config/CLIConfiguration.ts deleted file mode 100644 index fb782e13..00000000 --- a/config/CLIConfiguration.ts +++ /dev/null @@ -1,644 +0,0 @@ -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 } 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.config && this.config.defaultAccount - ? this.config.defaultAccount - : null; - } - - // TODO a util that returns the account to use, respecting the values set in - // "defaultAccountOverrides" - // Example "defaultAccountOverrides": - // - /src/brodgers/customer-project-1: customer-account1 - // - /src/brodgers/customer-project-2: customer-account2 - // "/src/brodgers/customer-project-1" is the path to the project dir - // "customer-account1" is the name of the account to use as the default for the specified dir - // These defaults take precedence over the standard default account specified in the config - getResolvedDefaultAccountForCWD( - nameOrId: string | number - ): CLIAccount_NEW | null { - return this.getAccount(nameOrId); - } - - 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..1dc6a724 100644 --- a/config/README.md +++ b/config/README.md @@ -1,3 +1,5 @@ +TODO: UPDATE THIS + # hubspot/local-dev-lib ## Config utils diff --git a/config/__tests__/environment.test.ts b/config/__tests__/environment.test.ts index 62b8015f..b18d2983 100644 --- a/config/__tests__/environment.test.ts +++ b/config/__tests__/environment.test.ts @@ -1,3 +1,5 @@ +// @TODO: update tests + import { loadConfigFromEnvironment } from '../environment'; import { ENVIRONMENT_VARIABLES } from '../../constants/environments'; import { PERSONAL_ACCESS_KEY_AUTH_METHOD } from '../../constants/auth'; diff --git a/config/config.ts b/config/config.ts deleted file mode 100644 index a4109bec..00000000 --- a/config/config.ts +++ /dev/null @@ -1,354 +0,0 @@ -import fs from 'fs-extra'; - -import { 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, - getLocalConfigFileDefaultPath, - getConfigAccountByIdentifier, - isConfigAccountValid, - getConfigAccountIndexById, - getConfigPathEnvironmentVariables, -} from './configUtils'; -import { CMS_PUBLISH_MODE } from '../constants/files'; -import { Environment } from '../types/Config'; - -export function localConfigFileExists(): boolean { - return Boolean(getLocalConfigFilePath()); -} - -export function globalConfigFileExists(): boolean { - return fs.existsSync(getGlobalConfigFilePath()); -} - -function getDefaultConfigFilePath(): string { - const globalConfigFilePath = getGlobalConfigFilePath(); - - if (fs.existsSync(globalConfigFilePath)) { - return globalConfigFilePath; - } - - const localConfigFilePath = getLocalConfigFilePath(); - - if (!localConfigFilePath) { - throw new Error('@TODO'); - } - - return localConfigFilePath; -} - -export function getConfigFilePath(): string { - const { configFilePathFromEnvironment } = getConfigPathEnvironmentVariables(); - - return configFilePathFromEnvironment || getDefaultConfigFilePath(); -} - -export function getConfig(): HubSpotConfig { - const { useEnvironmentConfig } = getConfigPathEnvironmentVariables(); - - if (useEnvironmentConfig) { - return buildConfigFromEnvironment(); - } - - const pathToRead = getConfigFilePath(); - - logger.debug(`@TODOReading config from ${pathToRead}`); - const configFileSource = readConfigFile(pathToRead); - - return parseConfig(configFileSource); -} - -export function isConfigValid(): boolean { - const config = getConfig(); - - if (config.accounts.length === 0) { - logger.log('@TODO'); - return false; - } - - const accountIdsMap: { [key: number]: boolean } = {}; - const accountNamesMap: { [key: string]: boolean } = {}; - - return config.accounts.every(account => { - if (!isConfigAccountValid(account)) { - logger.log('@TODO'); - return false; - } - if (accountIdsMap[account.accountId]) { - logger.log('@TODO'); - return false; - } - if (account.name) { - if (accountNamesMap[account.name.toLowerCase()]) { - logger.log('@TODO'); - return false; - } - if (/\s+/.test(account.name)) { - logger.log('@TODO'); - return false; - } - accountNamesMap[account.name] = true; - } - - accountIdsMap[account.accountId] = true; - return true; - }); -} - -export function createEmptyConfigFile(useGlobalConfig = false): void { - const { configFilePathFromEnvironment } = getConfigPathEnvironmentVariables(); - const defaultPath = useGlobalConfig - ? getGlobalConfigFilePath() - : getLocalConfigFileDefaultPath(); - - const pathToWrite = configFilePathFromEnvironment || defaultPath; - - writeConfigFile({ accounts: [] }, pathToWrite); -} - -export function deleteConfigFile(): void { - const pathToDelete = getConfigFilePath(); - fs.unlinkSync(pathToDelete); -} - -export function getConfigAccountById(accountId: number): HubSpotConfigAccount { - const { accounts } = getConfig(); - - const account = getConfigAccountByIdentifier( - accounts, - 'accountId', - accountId - ); - - if (!account) { - throw new Error('@TODO account not found'); - } - - return account; -} - -export function getConfigAccountByName( - accountName: string -): HubSpotConfigAccount { - const { accounts } = getConfig(); - - const account = getConfigAccountByIdentifier(accounts, 'name', accountName); - - if (!account) { - throw new Error('@TODO account not found'); - } - - return account; -} - -export function getConfigDefaultAccount(): HubSpotConfigAccount { - const { accounts, defaultAccount } = getConfig(); - - if (!defaultAccount) { - throw new Error('@TODO no default account'); - } - - const account = getConfigAccountByIdentifier( - accounts, - 'accountId', - defaultAccount - ); - - if (!account) { - throw new Error('@TODO no default account'); - } - - return account; -} - -export function getAllConfigAccounts(): HubSpotConfigAccount[] { - const { accounts } = getConfig(); - - return accounts; -} - -export function getConfigAccountEnvironment(accountId?: number): Environment { - if (accountId) { - const account = getConfigAccountById(accountId); - return account.env; - } - const defaultAccount = getConfigDefaultAccount(); - return defaultAccount.env; -} - -// @TODO: Add logger debugs? -export function addConfigAccount(accountToAdd: HubSpotConfigAccount): void { - if (!isConfigAccountValid(accountToAdd)) { - throw new Error('@TODO'); - } - - const config = getConfig(); - - const accountInConfig = getConfigAccountByIdentifier( - config.accounts, - 'accountId', - accountToAdd.accountId - ); - - if (accountInConfig) { - throw new Error('@TODO account already exists'); - } - - config.accounts.push(accountToAdd); - - writeConfigFile(config, getConfigFilePath()); -} - -export function updateConfigAccount( - updatedAccount: HubSpotConfigAccount -): void { - if (!isConfigAccountValid(updatedAccount)) { - throw new Error('@TODO'); - } - - const config = getConfig(); - - const accountIndex = getConfigAccountIndexById( - config.accounts, - updatedAccount.accountId - ); - - if (accountIndex < 0) { - throw new Error('@TODO account not found'); - } - - config.accounts[accountIndex] = updatedAccount; - - writeConfigFile(config, getConfigFilePath()); -} - -export function setConfigAccountAsDefault( - accountIdentifier: number | string -): void { - const config = getConfig(); - - const identifierAsNumber = - typeof accountIdentifier === 'number' - ? accountIdentifier - : parseInt(accountIdentifier); - const isId = !isNaN(identifierAsNumber); - - const account = getConfigAccountByIdentifier( - config.accounts, - isId ? 'accountId' : 'name', - isId ? identifierAsNumber : accountIdentifier - ); - - if (!account) { - throw new Error('@TODO account not found'); - } - - config.defaultAccount = account.accountId; - writeConfigFile(config, getConfigFilePath()); -} - -export function renameConfigAccount( - currentName: string, - newName: string -): void { - const config = getConfig(); - - const account = getConfigAccountByIdentifier( - config.accounts, - 'name', - currentName - ); - - if (!account) { - throw new Error('@TODO account not found'); - } - - const duplicateAccount = getConfigAccountByIdentifier( - config.accounts, - 'name', - newName - ); - - if (duplicateAccount) { - throw new Error('@TODO account name already exists'); - } - - account.name = newName; - - writeConfigFile(config, getConfigFilePath()); -} - -export function removeAccountFromConfig(accountId: number): void { - const config = getConfig(); - - const index = getConfigAccountIndexById(config.accounts, accountId); - - if (index < 0) { - throw new Error('@TODO account does not exist'); - } - - config.accounts.splice(index, 1); - - writeConfigFile(config, getConfigFilePath()); -} - -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('@TODO timeout must be greater than min'); - } - - const config = getConfig(); - - config.httpTimeout = parsedTimeout; - - writeConfigFile(config, getConfigFilePath()); -} - -export function updateAllowUsageTracking(isAllowed: boolean): void { - const config = getConfig(); - - config.allowUsageTracking = isAllowed; - - writeConfigFile(config, getConfigFilePath()); -} - -export function updateDefaultCmsPublishMode( - cmsPublishMode: CmsPublishMode -): void { - if ( - !cmsPublishMode || - !Object.values(CMS_PUBLISH_MODE).includes(cmsPublishMode) - ) { - throw new Error('@TODO invalid CMS publihs mode'); - } - - const config = getConfig(); - - config.defaultCmsPublishMode = cmsPublishMode; - - writeConfigFile(config, getConfigFilePath()); -} - -export function isConfigFlagEnabled( - flag: ConfigFlag, - defaultValue: boolean -): boolean { - const config = getConfig(); - - if (typeof config[flag] === 'undefined') { - return defaultValue; - } - - return Boolean(config[flag]); -} diff --git a/config/configFile.ts b/config/configFile.ts deleted file mode 100644 index 20c3a5fd..00000000 --- a/config/configFile.ts +++ /dev/null @@ -1,120 +0,0 @@ -import fs from 'fs-extra'; -import path from 'path'; -import os from 'os'; -import yaml from 'js-yaml'; -import { logger } from '../lib/logger'; -import { - HUBSPOT_CONFIGURATION_FILE, - HUBSPOT_CONFIGURATION_FOLDER, -} from '../constants/config'; -import { getOrderedConfig } from './configUtils_OLD'; -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 path.join( - os.homedir(), - HUBSPOT_CONFIGURATION_FOLDER, - HUBSPOT_CONFIGURATION_FILE - ); -} - -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_OLD.ts b/config/configUtils_OLD.ts deleted file mode 100644 index 04c42265..00000000 --- a/config/configUtils_OLD.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/environment.ts b/config/environment.ts deleted file mode 100644 index b345ad80..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_OLD'; -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 c29d6e33..5ffb09e6 100644 --- a/config/index.ts +++ b/config/index.ts @@ -1,268 +1,366 @@ -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 { 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, + getLocalConfigFileDefaultPath, + getConfigAccountByIdentifier, + isConfigAccountValid, + getConfigAccountIndexById, + getConfigPathEnvironmentVariables, + getAccountIdentifierAndType, +} from './utils'; +import { CMS_PUBLISH_MODE } from '../constants/files'; +import { Environment } from '../types/Config'; -// 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 localConfigFileExists(): boolean { + return Boolean(getLocalConfigFilePath()); } -export function getAndLoadConfigIfNeeded( - options?: CLIOptions -): Partial | null { - if (CLIConfiguration.isActive()) { - return CLIConfiguration.config; - } - return config_DEPRECATED.getAndLoadConfigIfNeeded(options); +export function globalConfigFileExists(): boolean { + return fs.existsSync(getGlobalConfigFilePath()); } -export function validateConfig(): boolean { - if (CLIConfiguration.isActive()) { - return CLIConfiguration.validate(); - } - return config_DEPRECATED.validateConfig(); -} +function getDefaultConfigFilePath(): string { + const globalConfigFilePath = getGlobalConfigFilePath(); -export function loadConfigFromEnvironment(): boolean { - if (CLIConfiguration.isActive()) { - return CLIConfiguration.useEnvConfig; + if (fs.existsSync(globalConfigFilePath)) { + return globalConfigFilePath; } - return Boolean(config_DEPRECATED.loadConfigFromEnvironment()); -} -export function createEmptyConfigFile( - options: { path?: string } = {}, - useHiddenConfig = false -): void { - if (useHiddenConfig) { - CLIConfiguration.write({ accounts: [] }); - } else { - return config_DEPRECATED.createEmptyConfigFile(options); - } -} + const localConfigFilePath = getLocalConfigFilePath(); -export function deleteEmptyConfigFile() { - if (CLIConfiguration.isActive()) { - return CLIConfiguration.delete(); + if (!localConfigFilePath) { + throw new Error('@TODO'); } - return config_DEPRECATED.deleteEmptyConfigFile(); + + return localConfigFilePath; } -export function getConfig(): CLIConfig | null { - if (CLIConfiguration.isActive()) { - return CLIConfiguration.config; - } - return config_DEPRECATED.getConfig(); +export function getConfigFilePath(): string { + const { configFilePathFromEnvironment } = getConfigPathEnvironmentVariables(); + + return configFilePathFromEnvironment || getDefaultConfigFilePath(); } -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 getConfig(): HubSpotConfig { + const { useEnvironmentConfig } = getConfigPathEnvironmentVariables(); + + if (useEnvironmentConfig) { + return buildConfigFromEnvironment(); } + + const pathToRead = getConfigFilePath(); + + logger.debug(`@TODOReading config from ${pathToRead}`); + const configFileSource = readConfigFile(pathToRead); + + return parseConfig(configFileSource); } -export function getConfigPath( - path?: string, - useHiddenConfig = false -): string | null { - if (useHiddenConfig || CLIConfiguration.isActive()) { - return getConfigFilePath(); +export function isConfigValid(): boolean { + const config = getConfig(); + + if (config.accounts.length === 0) { + logger.log('@TODO'); + return false; } - return config_DEPRECATED.getConfigPath(path); + + const accountIdsMap: { [key: number]: boolean } = {}; + const accountNamesMap: { [key: string]: boolean } = {}; + + return config.accounts.every(account => { + if (!isConfigAccountValid(account)) { + logger.log('@TODO'); + return false; + } + if (accountIdsMap[account.accountId]) { + logger.log('@TODO'); + return false; + } + if (account.name) { + if (accountNamesMap[account.name.toLowerCase()]) { + logger.log('@TODO'); + return false; + } + if (/\s+/.test(account.name)) { + logger.log('@TODO'); + return false; + } + accountNamesMap[account.name] = true; + } + + accountIdsMap[account.accountId] = true; + return true; + }); } -export function configFileExists(useHiddenConfig?: boolean) { - return useHiddenConfig - ? newConfigFileExists() - : Boolean(config_DEPRECATED.getConfigPath()); +export function createEmptyConfigFile(useGlobalConfig = false): void { + const { configFilePathFromEnvironment } = getConfigPathEnvironmentVariables(); + const defaultPath = useGlobalConfig + ? getGlobalConfigFilePath() + : getLocalConfigFileDefaultPath(); + + const pathToWrite = configFilePathFromEnvironment || defaultPath; + + writeConfigFile({ accounts: [] }, pathToWrite); } -export function getAccountConfig(accountId?: number): CLIAccount | null { - if (CLIConfiguration.isActive()) { - return CLIConfiguration.getConfigForAccount(accountId); - } - return config_DEPRECATED.getAccountConfig(accountId) || null; +export function deleteConfigFile(): void { + const pathToDelete = getConfigFilePath(); + fs.unlinkSync(pathToDelete); } -export function accountNameExistsInConfig(name: string): boolean { - if (CLIConfiguration.isActive()) { - return CLIConfiguration.isAccountInConfig(name); +export function getConfigAccountById(accountId: number): HubSpotConfigAccount { + const { accounts } = getConfig(); + + const account = getConfigAccountByIdentifier( + accounts, + 'accountId', + accountId + ); + + if (!account) { + throw new Error('@TODO account not found'); } - return config_DEPRECATED.accountNameExistsInConfig(name); + + return account; } -export function updateAccountConfig( - configOptions: Partial -): FlatAccountFields | null { - const accountIdentifier = getAccountIdentifier(configOptions); - if (CLIConfiguration.isActive()) { - return CLIConfiguration.addOrUpdateAccount({ - ...configOptions, - accountId: accountIdentifier, - }); +export function getConfigAccountByName( + accountName: string +): HubSpotConfigAccount { + const { accounts } = getConfig(); + + const account = getConfigAccountByIdentifier(accounts, 'name', accountName); + + if (!account) { + throw new Error('@TODO account not found'); } - 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 getConfigDefaultAccount(): HubSpotConfigAccount { + const { accounts, defaultAccount } = getConfig(); + + if (!defaultAccount) { + throw new Error('@TODO no default account'); } -} -export async function renameAccount( - currentName: string, - newName: string -): Promise { - if (CLIConfiguration.isActive()) { - CLIConfiguration.renameAccount(currentName, newName); - } else { - config_DEPRECATED.renameAccount(currentName, newName); + const account = getConfigAccountByIdentifier( + accounts, + 'accountId', + defaultAccount + ); + + if (!account) { + throw new Error('@TODO no default account'); } + + 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( + accountIdentifier?: number | string +): Environment { + if (accountIdentifier) { + const config = getConfig(); + + const { identifier, identifierType } = + getAccountIdentifierAndType(accountIdentifier); + + const account = getConfigAccountByIdentifier( + config.accounts, + identifierType, + 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()) { - CLIConfiguration.removeAccountFromConfig(accountName); - } else { - config_DEPRECATED.deleteAccount(accountName); +// @TODO: Add logger debugs? +export function addConfigAccount(accountToAdd: HubSpotConfigAccount): void { + if (!isConfigAccountValid(accountToAdd)) { + throw new Error('@TODO'); } -} -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, + 'accountId', + accountToAdd.accountId + ); + + if (accountInConfig) { + throw new Error('@TODO account already exists'); } + + 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('@TODO'); } -} -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('@TODO account not found'); } + + config.accounts[accountIndex] = updatedAccount; + + writeConfigFile(config, getConfigFilePath()); } -export function isConfigFlagEnabled(flag: keyof CLIConfig): boolean { - if (CLIConfiguration.isActive()) { - return CLIConfiguration.isConfigFlagEnabled(flag); +export function setConfigAccountAsDefault( + accountIdentifier: number | string +): void { + const config = getConfig(); + + const { identifier, identifierType } = + getAccountIdentifierAndType(accountIdentifier); + + const account = getConfigAccountByIdentifier( + config.accounts, + identifierType, + identifier + ); + + if (!account) { + throw new Error('@TODO account not found'); } - 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, + 'name', + currentName + ); + + if (!account) { + throw new Error('@TODO account not found'); } - return config_DEPRECATED.isTrackingAllowed(); -} -export function getEnv(nameOrId?: string | number) { - if (CLIConfiguration.isActive()) { - return CLIConfiguration.getEnv(nameOrId); + const duplicateAccount = getConfigAccountByIdentifier( + config.accounts, + 'name', + newName + ); + + if (duplicateAccount) { + throw new Error('@TODO account name already exists'); } - 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('@TODO account does not exist'); } - return config_DEPRECATED.getAccountType(accountType, sandboxAccountType); + + config.accounts.splice(index, 1); + + writeConfigFile(config, getConfigFilePath()); } -export function getConfigDefaultAccount(): string | number | null | undefined { - if (CLIConfiguration.isActive()) { - return CLIConfiguration.getDefaultAccount(); +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('@TODO timeout must be greater than min'); } - 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('@TODO invalid CMS publihs mode'); } - return config_DEPRECATED.updateDefaultCmsPublishMode(cmsPublishMode); + + const config = getConfig(); + + config.defaultCmsPublishMode = cmsPublishMode; + + writeConfigFile(config, getConfigFilePath()); } -// 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; +export function isConfigFlagEnabled( + flag: ConfigFlag, + defaultValue: boolean +): boolean { + const config = getConfig(); + + if (typeof config[flag] === 'undefined') { + return defaultValue; + } + + return Boolean(config[flag]); +} diff --git a/config/configUtils.ts b/config/utils.ts similarity index 95% rename from config/configUtils.ts rename to config/utils.ts index 7758b6a5..58dd97a3 100644 --- a/config/configUtils.ts +++ b/config/utils.ts @@ -340,3 +340,18 @@ export function isConfigAccountValid(account: HubSpotConfigAccount) { return false; } + +export function getAccountIdentifierAndType( + accountIdentifier: string | number +): { identifier: string | number; identifierType: 'name' | 'accountId' } { + const identifierAsNumber = + typeof accountIdentifier === 'number' + ? accountIdentifier + : parseInt(accountIdentifier); + const isId = !isNaN(identifierAsNumber); + + return { + identifier: isId ? identifierAsNumber : accountIdentifier, + identifierType: isId ? 'accountId' : 'name', + }; +} diff --git a/http/getAxiosConfig.ts b/http/getAxiosConfig.ts index 2f4ad454..c66bb330 100644 --- a/http/getAxiosConfig.ts +++ b/http/getAxiosConfig.ts @@ -1,5 +1,5 @@ import { version } from '../package.json'; -import { getAndLoadConfigIfNeeded } from '../config'; +import { getConfig } from '../config'; import { getHubSpotApiOrigin } from '../lib/urls'; import { HttpOptions } from '../types/Http'; import { AxiosRequestConfig } from 'axios'; @@ -48,7 +48,7 @@ const DEFAULT_TRANSITIONAL = { export function getAxiosConfig(options: HttpOptions): AxiosRequestConfig { const { env, localHostOverride, headers, ...rest } = options; - const config = getAndLoadConfigIfNeeded(); + const config = getConfig(); let httpTimeout = 15000; let httpUseLocalhost = false; diff --git a/http/index.ts b/http/index.ts index 6bebea8a..ca9318b0 100644 --- a/http/index.ts +++ b/http/index.ts @@ -3,16 +3,16 @@ 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'; const i18nKey = 'http.index'; @@ -27,15 +27,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,13 +83,9 @@ 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 }) @@ -99,17 +96,22 @@ async function withAuth( } if (authType === 'oauth2') { - return withOauth(accountId, accountConfig, axiosConfig); + return withOauth(account, axiosConfig); } - const { params } = axiosConfig; - return { - ...axiosConfig, - params: { - ...params, - hapikey: apiKey, - }, - }; + if (authType === 'apikey') { + const { params } = axiosConfig; + + return { + ...axiosConfig, + params: { + ...params, + hapikey: account.apiKey, + }, + }; + } + + throw new Error('@TODO: invalid aut type'); } async function getRequest( 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/oauth.ts b/lib/oauth.ts index f4ea4881..e4afe88f 100644 --- a/lib/oauth.ts +++ b/lib/oauth.ts @@ -1,47 +1,34 @@ 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 2ceb8909..064d4001 100644 --- a/lib/personalAccessKey.ts +++ b/lib/personalAccessKey.ts @@ -3,19 +3,22 @@ import { ENVIRONMENTS } from '../constants/environments'; import { PERSONAL_ACCESS_KEY_AUTH_METHOD } from '../constants/auth'; import { fetchAccessToken } from '../api/localDevAuth'; import { fetchSandboxHubData } from '../api/sandboxHubs'; -import { CLIAccount, PersonalAccessKeyAccount } from '../types/Accounts'; +import { + HubSpotConfigAccount, + PersonalAccessKeyConfigAccount, +} from '../types/Accounts'; import { Environment } from '../types/Config'; import { - getAccountConfig, - updateAccountConfig, - writeConfig, - getEnv, - updateDefaultAccount, + getConfigAccountById, + getConfigAccountByName, + updateConfigAccount, + getConfigAccountEnvironment, + 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'; @@ -53,49 +56,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); } @@ -112,18 +106,15 @@ 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('@TODO'); + } - const accessTokenResponse = await getNewAccessToken( - accountId, - personalAccessKey, - auth?.tokenInfo?.expiresAt, - env - ); + const accessTokenResponse = await getNewAccessToken(account); return accessTokenResponse; } @@ -131,11 +122,15 @@ 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('@TODO'); + } + + const { auth } = account; const authTokenInfo = auth && auth.tokenInfo; const authDataExists = authTokenInfo && auth?.tokenInfo?.accessToken; @@ -144,15 +139,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( @@ -174,9 +164,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 { @@ -217,22 +210,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/types/Accounts.ts b/types/Accounts.ts index c2b2eeac..bd6d653f 100644 --- a/types/Accounts.ts +++ b/types/Accounts.ts @@ -24,11 +24,6 @@ export type DeprecatedHubSpotConfigAccountFields = { portalId: number; }; -export type GenericAccount = { - portalId?: number; - accountId?: number; -}; - export type AccountType = ValueOf; export type TokenInfo = { @@ -41,6 +36,9 @@ export interface PersonalAccessKeyConfigAccount extends BaseHubSpotConfigAccount { authType: typeof PERSONAL_ACCESS_KEY_AUTH_METHOD.value; personalAccessKey: string; + auth: { + tokenInfo: TokenInfo; + }; } export interface OAuthConfigAccount extends BaseHubSpotConfigAccount { @@ -84,11 +82,6 @@ export type EnabledFeaturesResponse = { enabledFeatures: { [key: string]: boolean }; }; -// export type UpdateAccountConfigOptions = -// Partial & { -// environment?: Environment; -// }; - export type PersonalAccessKeyOptions = { accountId: number; personalAccessKey: string; @@ -121,18 +114,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 b4bfaa0f..f55baf1d 100644 --- a/types/Config.ts +++ b/types/Config.ts @@ -24,29 +24,8 @@ export type DeprecatedHubSpotConfigFields = { defaultMode?: CmsPublishMode; }; -// 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 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; From 05699b3c49f4e0fac1bfa3dab539ecbdc3f838f5 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Mon, 10 Feb 2025 13:32:12 -0500 Subject: [PATCH 15/70] Fix TS build --- config/utils.ts | 3 +++ lib/oauth.ts | 1 - lib/personalAccessKey.ts | 6 +----- utils/accounts.ts | 29 ----------------------------- 4 files changed, 4 insertions(+), 35 deletions(-) delete mode 100644 utils/accounts.ts diff --git a/config/utils.ts b/config/utils.ts index 58dd97a3..ed841613 100644 --- a/config/utils.ts +++ b/config/utils.ts @@ -264,6 +264,9 @@ export function buildConfigFromEnvironment(): HubSpotConfig { personalAccessKey, env, name: accountIdVar, + auth: { + tokenInfo: {}, + }, }; } else if (clientId && clientSecret && refreshToken) { account = { diff --git a/lib/oauth.ts b/lib/oauth.ts index e4afe88f..00823c76 100644 --- a/lib/oauth.ts +++ b/lib/oauth.ts @@ -1,5 +1,4 @@ import { OAuth2Manager } from '../models/OAuth2Manager'; -import { AUTH_METHODS } from '../constants/auth'; import { OAuthConfigAccount } from '../types/Accounts'; import { logger } from './logger'; import { updateConfigAccount } from '../config'; diff --git a/lib/personalAccessKey.ts b/lib/personalAccessKey.ts index 064d4001..cb16919b 100644 --- a/lib/personalAccessKey.ts +++ b/lib/personalAccessKey.ts @@ -3,16 +3,12 @@ import { ENVIRONMENTS } from '../constants/environments'; import { PERSONAL_ACCESS_KEY_AUTH_METHOD } from '../constants/auth'; import { fetchAccessToken } from '../api/localDevAuth'; import { fetchSandboxHubData } from '../api/sandboxHubs'; -import { - HubSpotConfigAccount, - PersonalAccessKeyConfigAccount, -} from '../types/Accounts'; +import { PersonalAccessKeyConfigAccount } from '../types/Accounts'; import { Environment } from '../types/Config'; import { getConfigAccountById, getConfigAccountByName, updateConfigAccount, - getConfigAccountEnvironment, setConfigAccountAsDefault, getConfigDefaultAccount, } from '../config'; 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; - } -} From 930ac7510729542ec99209f6e578215dadbaa0dc Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Mon, 10 Feb 2025 13:37:46 -0500 Subject: [PATCH 16/70] remove unneeded types --- types/Accounts.ts | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/types/Accounts.ts b/types/Accounts.ts index bd6d653f..d8ef0866 100644 --- a/types/Accounts.ts +++ b/types/Accounts.ts @@ -82,27 +82,6 @@ export type EnabledFeaturesResponse = { enabledFeatures: { [key: string]: boolean }; }; -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; From b541617649e687a6ad0b609aceec573e646cf14b Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Mon, 10 Feb 2025 16:35:59 -0500 Subject: [PATCH 17/70] configUtils tests --- config/__tests__/CLIConfiguration.test.ts | 150 --------- config/__tests__/configFile.test.ts | 167 --------- config/__tests__/configUtils.test.ts | 127 ------- config/__tests__/environment.test.ts | 51 --- config/__tests__/utils.test.ts | 392 ++++++++++++++++++++++ config/index.ts | 22 +- config/utils.ts | 107 ++++-- constants/config.ts | 17 + constants/environments.ts | 17 - types/Accounts.ts | 2 +- 10 files changed, 493 insertions(+), 559 deletions(-) delete mode 100644 config/__tests__/CLIConfiguration.test.ts delete mode 100644 config/__tests__/configFile.test.ts delete mode 100644 config/__tests__/configUtils.test.ts delete mode 100644 config/__tests__/environment.test.ts create mode 100644 config/__tests__/utils.test.ts 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__/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__/environment.test.ts b/config/__tests__/environment.test.ts deleted file mode 100644 index b18d2983..00000000 --- a/config/__tests__/environment.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -// @TODO: update tests - -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__/utils.test.ts b/config/__tests__/utils.test.ts new file mode 100644 index 00000000..49661fa3 --- /dev/null +++ b/config/__tests__/utils.test.ts @@ -0,0 +1,392 @@ +import findup from 'findup-sync'; +import fs from 'fs-extra'; +import { + getGlobalConfigFilePath, + getLocalConfigFilePath, + getLocalConfigFileDefaultPath, + 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 } 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', () => { + describe('getGlobalConfigFilePath()', () => { + it('returns the global config file path', () => { + const globalConfigFilePath = getGlobalConfigFilePath(); + expect(globalConfigFilePath).toBeDefined(); + expect(globalConfigFilePath).toContain('.hubspot-cli/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('getLocalConfigFileDefaultPath()', () => { + it('returns the default config path in current directory', () => { + const mockCwdPath = '/mock/cwd'; + mockCwd.mockReturnValue(mockCwdPath); + + const defaultPath = getLocalConfigFileDefaultPath(); + 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); + cleanupEnvironmentVariables(); + }); + + it('throws when both environment variables are set', () => { + process.env[ENVIRONMENT_VARIABLES.HUBSPOT_CONFIG_PATH] = 'path'; + process.env[ENVIRONMENT_VARIABLES.USE_ENVIRONMENT_CONFIG] = 'true'; + + expect(() => getConfigPathEnvironmentVariables()).toThrow(); + cleanupEnvironmentVariables(); + }); + }); + + 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', () => { + // eslint-disable-next-line @typescript-eslint/no-empty-function + mockFs.ensureFileSync.mockImplementation(() => {}); + // eslint-disable-next-line @typescript-eslint/no-empty-function + mockFs.writeFileSync.mockImplementation(() => {}); + + 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 }], + }); + cleanupEnvironmentVariables(); + }); + + 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], + }); + cleanupEnvironmentVariables(); + }); + + it('throws when required variables missing', () => { + expect(() => { + process.env[ENVIRONMENT_VARIABLES.HUBSPOT_ACCOUNT_ID] = '123'; + process.env[ENVIRONMENT_VARIABLES.HUBSPOT_ENVIRONMENT] = 'qa'; + buildConfigFromEnvironment(); + }).toThrow(); + + cleanupEnvironmentVariables(); + }); + }); + + 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/index.ts b/config/index.ts index 5ffb09e6..c1eeda41 100644 --- a/config/index.ts +++ b/config/index.ts @@ -17,7 +17,7 @@ import { isConfigAccountValid, getConfigAccountIndexById, getConfigPathEnvironmentVariables, - getAccountIdentifierAndType, + getConfigAccountByInferredIdentifier, } from './utils'; import { CMS_PUBLISH_MODE } from '../constants/files'; import { Environment } from '../types/Config'; @@ -177,17 +177,13 @@ export function getAllConfigAccounts(): HubSpotConfigAccount[] { } export function getConfigAccountEnvironment( - accountIdentifier?: number | string + identifier?: number | string ): Environment { - if (accountIdentifier) { + if (identifier) { const config = getConfig(); - const { identifier, identifierType } = - getAccountIdentifierAndType(accountIdentifier); - - const account = getConfigAccountByIdentifier( + const account = getConfigAccountByInferredIdentifier( config.accounts, - identifierType, identifier ); @@ -245,17 +241,11 @@ export function updateConfigAccount( writeConfigFile(config, getConfigFilePath()); } -export function setConfigAccountAsDefault( - accountIdentifier: number | string -): void { +export function setConfigAccountAsDefault(identifier: number | string): void { const config = getConfig(); - const { identifier, identifierType } = - getAccountIdentifierAndType(accountIdentifier); - - const account = getConfigAccountByIdentifier( + const account = getConfigAccountByInferredIdentifier( config.accounts, - identifierType, identifier ); diff --git a/config/utils.ts b/config/utils.ts index ed841613..eff28d88 100644 --- a/config/utils.ts +++ b/config/utils.ts @@ -9,8 +9,8 @@ import { HUBSPOT_CONFIGURATION_FILE, HUBSPOT_ACCOUNT_TYPES, DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME, + ENVIRONMENT_VARIABLES, } from '../constants/config'; -import { ENVIRONMENT_VARIABLES } from '../constants/environments'; import { PERSONAL_ACCESS_KEY_AUTH_METHOD, API_KEY_AUTH_METHOD, @@ -20,7 +20,12 @@ import { import { HubSpotConfig, DeprecatedHubSpotConfigFields } from '../types/Config'; import { FileSystemError } from '../models/FileSystemError'; import { logger } from '../lib/logger'; -import { HubSpotConfigAccount, AccountType } from '../types/Accounts'; +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'; @@ -94,13 +99,27 @@ export function removeUndefinedFieldsFromConfigAccount< } }); - if ('auth' in account && typeof account.auth === 'object') { - Object.keys(account.auth).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; @@ -179,17 +198,30 @@ export function normalizeParsedConfig( ): HubSpotConfig { if (parsedConfig.portals) { parsedConfig.accounts = parsedConfig.portals.map(account => { - account.accountId = account.portalId; + if (account.portalId) { + account.accountId = account.portalId; + delete account.portalId; + } return account; }); + delete parsedConfig.portals; } if (parsedConfig.defaultPortal) { - parsedConfig.defaultAccount = parseInt(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; } parsedConfig.accounts.forEach(account => { @@ -305,6 +337,21 @@ export function buildConfigFromEnvironment(): HubSpotConfig { }; } +export function getAccountIdentifierAndType( + accountIdentifier: string | number +): { identifier: string | number; identifierType: 'name' | 'accountId' } { + const identifierAsNumber = + typeof accountIdentifier === 'number' + ? accountIdentifier + : parseInt(accountIdentifier); + const isId = !isNaN(identifierAsNumber); + + return { + identifier: isId ? identifierAsNumber : accountIdentifier, + identifierType: isId ? 'accountId' : 'name', + }; +} + export function getConfigAccountByIdentifier( accounts: Array, identifierFieldName: 'name' | 'accountId', @@ -313,6 +360,15 @@ export function getConfigAccountByIdentifier( 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: string | number @@ -320,7 +376,9 @@ export function getConfigAccountIndexById( return accounts.findIndex(account => account.accountId === id); } -export function isConfigAccountValid(account: HubSpotConfigAccount) { +export function isConfigAccountValid( + account: Partial +): boolean { if (!account || typeof account !== 'object') { return false; } @@ -329,32 +387,21 @@ export function isConfigAccountValid(account: HubSpotConfigAccount) { return false; } + if (!account.accountId) { + return false; + } + if (account.authType === PERSONAL_ACCESS_KEY_AUTH_METHOD.value) { - return 'personalAccessKey' in account && account.personalAccessKey; + return 'personalAccessKey' in account && Boolean(account.personalAccessKey); } if (account.authType === OAUTH_AUTH_METHOD.value) { - return 'auth' in account && account.auth; + return 'auth' in account && Boolean(account.auth); } if (account.authType === API_KEY_AUTH_METHOD.value) { - return 'apiKey' in account && account.apiKey; + return 'apiKey' in account && Boolean(account.apiKey); } return false; } - -export function getAccountIdentifierAndType( - accountIdentifier: string | number -): { identifier: string | number; identifierType: 'name' | 'accountId' } { - const identifierAsNumber = - typeof accountIdentifier === 'number' - ? accountIdentifier - : parseInt(accountIdentifier); - const isId = !isNaN(identifierAsNumber); - - return { - identifier: isId ? identifierAsNumber : accountIdentifier, - identifierType: isId ? 'accountId' : 'name', - }; -} diff --git a/constants/config.ts b/constants/config.ts index 3fca6cac..8565618a 100644 --- a/constants/config.ts +++ b/constants/config.ts @@ -26,3 +26,20 @@ export const HUBSPOT_ACCOUNT_TYPE_STRINGS = { export const CONFIG_FLAGS = { USE_CUSTOM_OBJECT_HUBFILE: 'useCustomObjectHubfile', } 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_CONFIG: 'USE_ENVIRONMENT_CONFIG', + HUBSPOT_CONFIG_PATH: 'HUBSPOT_CONFIG_PATH', +} as const; diff --git a/constants/environments.ts b/constants/environments.ts index a6c5c449..a8bff37b 100644 --- a/constants/environments.ts +++ b/constants/environments.ts @@ -2,20 +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', - HTTP_USE_LOCALHOST: 'HTTP_USE_LOCALHOST', - ALLOW_USAGE_TRACKING: 'ALLOW_USAGE_TRACKING', - DEFAULT_CMS_PUBLISH_MODE: 'DEFUALT_CMS_PUBLISH_MODE', - USE_ENVIRONMENT_CONFIG: 'USE_ENVIRONMENT_CONFIG', - HUBSPOT_CONFIG_PATH: 'HUBSPOT_CONFIG_PATH', -} as const; diff --git a/types/Accounts.ts b/types/Accounts.ts index d8ef0866..e05e54cc 100644 --- a/types/Accounts.ts +++ b/types/Accounts.ts @@ -21,7 +21,7 @@ interface BaseHubSpotConfigAccount { } export type DeprecatedHubSpotConfigAccountFields = { - portalId: number; + portalId?: number; }; export type AccountType = ValueOf; From e415848cbe106264165fc05c27f8503cf7cb9dd0 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Mon, 10 Feb 2025 17:20:57 -0500 Subject: [PATCH 18/70] Config WIP --- config/__tests__/config.test.ts | 941 ++++++++-------------------- config/__tests__/config_old.test.ts | 808 ++++++++++++++++++++++++ config/__tests__/utils.test.ts | 14 +- 3 files changed, 1071 insertions(+), 692 deletions(-) create mode 100644 config/__tests__/config_old.test.ts diff --git a/config/__tests__/config.test.ts b/config/__tests__/config.test.ts index eca433d9..1f3f6a9b 100644 --- a/config/__tests__/config.test.ts +++ b/config/__tests__/config.test.ts @@ -1,808 +1,377 @@ +import findup from 'findup-sync'; import fs from 'fs-extra'; + 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 { getCwd } from '../../lib/path'; 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, + getLocalConfigFileDefaultPath, +} from '../utils'; +import { ENVIRONMENT_VARIABLES } from '../../constants/config'; +import * as utils from '../utils'; + +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: { - 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: 123, + env: 'qa', + name: '123', + 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: 123, + env: 'qa', + name: '123', + authType: API_KEY_AUTH_METHOD.value, + accountType: undefined, + apiKey: 'test-api-key', +}; -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 cleanupEnvironmentVariables() { + Object.keys(ENVIRONMENT_VARIABLES).forEach(key => { + delete process.env[key]; + }); } -describe('config/config', () => { - const globalConsole = global.console; - beforeAll(() => { - global.console.error = jest.fn(); - global.console.debug = jest.fn(); +describe('config/index', () => { + beforeEach(() => { + cleanupEnvironmentVariables(); }); - afterAll(() => { - global.console = globalConsole; + + afterEach(() => { + cleanupEnvironmentVariables(); }); - describe('setConfig()', () => { - beforeEach(() => { - setConfig(CONFIG); + describe('localConfigFileExists()', () => { + it('returns true when local config exists', () => { + mockFindup.mockReturnValueOnce(getLocalConfigFileDefaultPath()); + 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 - ); + it('returns local path when global does not exist', () => { + mockFs.existsSync.mockReturnValueOnce(false); + mockFindup.mockReturnValueOnce(getLocalConfigFileDefaultPath()); + expect(getConfigFilePath()).toBe(getLocalConfigFileDefaultPath()); }); + }); - describe('when defaultPortal is a portalId', () => { - beforeEach(() => { - process.env = {}; - setConfig({ - defaultPortal: PERSONAL_ACCESS_KEY_CONFIG.portalId, - portals: PORTALS, - }); + describe('getConfig()', () => { + it('returns environment config when enabled', () => { + process.env[ENVIRONMENT_VARIABLES.USE_ENVIRONMENT_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('returns defaultPortal from config', () => { - expect(getAccountId() || undefined).toEqual( - PERSONAL_ACCESS_KEY_CONFIG.portalId - ); - }); + it('returns parsed config from file', () => { + mockFs.existsSync.mockReturnValueOnce(true); + mockFs.readFileSync.mockReturnValueOnce('test-config-content'); + jest.spyOn(utils, 'parseConfig').mockReturnValueOnce(CONFIG); + + expect(getConfig()).toEqual(CONFIG); }); }); - describe('updateDefaultAccount()', () => { - const myPortalName = 'Foo'; + describe('isConfigValid()', () => { + it('returns true for valid config', () => { + mockFs.existsSync.mockReturnValueOnce(true); + mockFs.readFileSync.mockReturnValueOnce('test-config-content'); + jest.spyOn(utils, 'parseConfig').mockReturnValueOnce(CONFIG); - beforeEach(() => { - updateDefaultAccount(myPortalName); + expect(isConfigValid()).toBe(true); }); - it('sets the defaultPortal in the config', () => { - const config = getConfig(); - expect(config ? getDefaultAccount(config) : null).toEqual(myPortalName); + it('returns false for config with no accounts', () => { + mockFs.existsSync.mockReturnValueOnce(true); + mockFs.readFileSync.mockReturnValueOnce('test-config-content'); + jest.spyOn(utils, 'parseConfig').mockReturnValueOnce({ accounts: [] }); + + expect(isConfigValid()).toBe(false); }); - }); - describe('deleteEmptyConfigFile()', () => { - it('does not delete config file if there are contents', () => { + it('returns false for config with duplicate account ids', () => { + mockFs.existsSync.mockReturnValueOnce(true); + mockFs.readFileSync.mockReturnValueOnce('test-config-content'); jest - .spyOn(fs, 'readFileSync') - .mockImplementation(() => 'defaultPortal: "test"'); - jest.spyOn(fs, 'existsSync').mockImplementation(() => true); - fs.unlinkSync = jest.fn(); + .spyOn(utils, 'parseConfig') + .mockReturnValueOnce({ accounts: [PAK_ACCOUNT, PAK_ACCOUNT] }); - deleteEmptyConfigFile(); - expect(fs.unlinkSync).not.toHaveBeenCalled(); + expect(isConfigValid()).toBe(false); }); + }); - it('deletes config file if empty', () => { - jest.spyOn(fs, 'readFileSync').mockImplementation(() => ''); - jest.spyOn(fs, 'existsSync').mockImplementation(() => true); - fs.unlinkSync = jest.fn(); + describe('createEmptyConfigFile()', () => { + it('creates global config when specified', () => { + // TODO: Implement test + }); - deleteEmptyConfigFile(); - expect(fs.unlinkSync).toHaveBeenCalled(); + it('creates local config by default', () => { + // TODO: Implement test }); }); - describe('updateAccountConfig()', () => { - const CONFIG = { - defaultPortal: PORTALS[0].name, - portals: PORTALS, - }; - - beforeEach(() => { - setConfig(CONFIG); - }); - - 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); - - expect( - getAccountByAuthType( - getConfig(), - modifiedPersonalAccessKeyConfig.authType - ).env - ).toEqual(newEnv); - }); - - 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); - - expect( - getAccountByAuthType( - getConfig(), - modifiedPersonalAccessKeyConfig.authType - ).name - ).toEqual(newName); - }); + describe('deleteConfigFile()', () => { + it('deletes the config file', () => {}); }); - describe('validateConfig()', () => { - const DEFAULT_PORTAL = PORTALS[0].name; + describe('getConfigAccountById()', () => { + it('returns account when found', () => { + // TODO: Implement test + }); - it('allows valid config', () => { - setConfig({ - defaultPortal: DEFAULT_PORTAL, - portals: PORTALS, - }); - expect(validateConfig()).toEqual(true); + it('throws when account not found', () => { + // TODO: Implement test }); + }); - it('does not allow duplicate portalIds', () => { - setConfig({ - defaultPortal: DEFAULT_PORTAL, - portals: [...PORTALS, PORTALS[0]], - }); - expect(validateConfig()).toEqual(false); + describe('getConfigAccountByName()', () => { + it('returns account when found', () => { + // TODO: Implement test }); - it('does not allow duplicate names', () => { - setConfig({ - defaultPortal: DEFAULT_PORTAL, - portals: [ - ...PORTALS, - { - ...PORTALS[0], - portalId: 123456789, - }, - ], - }); - expect(validateConfig()).toEqual(false); + it('throws when account not found', () => { + // TODO: Implement test }); + }); - it('does not allow names with spaces', () => { - setConfig({ - defaultPortal: DEFAULT_PORTAL, - portals: [ - { - ...PORTALS[0], - name: 'A NAME WITH SPACES', - }, - ], - }); - expect(validateConfig()).toEqual(false); + describe('getConfigDefaultAccount()', () => { + it('returns default account when set', () => { + // TODO: Implement test }); - it('allows multiple portals with no name', () => { - setConfig({ - defaultPortal: DEFAULT_PORTAL, - portals: [ - { - ...PORTALS[0], - name: undefined, - }, - { - ...PORTALS[1], - name: undefined, - }, - ], - }); - expect(validateConfig()).toEqual(true); + it('throws when no default account', () => { + // TODO: Implement test }); }); - 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(); - }); - - it('properly loads portal id value', () => { - expect(getAccountIdentifier(portalConfig)).toEqual(portalId); - }); - - it('properly loads client id value', () => { - expect(portalConfig?.auth.clientId).toEqual(clientId); - }); - - it('properly loads client secret value', () => { - expect(portalConfig?.auth.clientSecret).toEqual(clientSecret); - }); - - it('properly loads refresh token value', () => { - expect(portalConfig?.auth?.tokenInfo?.refreshToken).toEqual( - refreshToken - ); - }); + describe('getAllConfigAccounts()', () => { + it('returns all accounts', () => { + // TODO: Implement test }); + }); - 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(); - }); - - 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(); - }); - - it('properly loads portal id value', () => { - expect(getAccountIdentifier(portalConfig)).toEqual(portalId); - }); - - it('properly loads api key value', () => { - expect(portalConfig.apiKey).toEqual(apiKey); - }); + describe('getConfigAccountEnvironment()', () => { + it('returns environment for specified account', () => { + // TODO: Implement test }); - 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(); - }); - - 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(); - }); - - it('properly loads portal id value', () => { - expect(getAccountIdentifier(portalConfig)).toEqual(portalId); - }); - - it('properly loads personal access key value', () => { - expect(portalConfig?.personalAccessKey).toEqual(personalAccessKey); - }); + it('returns default account environment when no identifier', () => { + // TODO: Implement test }); }); - describe('getAccountType()', () => { - it('returns STANDARD when no accountType or sandboxAccountType is specified', () => { - expect(getAccountType()).toBe(HUBSPOT_ACCOUNT_TYPES.STANDARD); + describe('addConfigAccount()', () => { + it('adds valid account to config', () => { + // TODO: Implement test }); - 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 - ); + + it('throws for invalid account', () => { + // TODO: Implement test }); - it('handles accountType arg correctly', () => { - expect(getAccountType(HUBSPOT_ACCOUNT_TYPES.STANDARD, 'DEVELOPER')).toBe( - HUBSPOT_ACCOUNT_TYPES.STANDARD - ); + + it('throws when account already exists', () => { + // TODO: Implement test }); }); - describe('getConfigPath()', () => { - let fsExistsSyncSpy: jest.SpyInstance; - - beforeAll(() => { - fsExistsSyncSpy = jest.spyOn(fs, 'existsSync').mockImplementation(() => { - return false; - }); + describe('updateConfigAccount()', () => { + it('updates existing account', () => { + // TODO: Implement test }); - afterAll(() => { - fsExistsSyncSpy.mockRestore(); + it('throws for invalid account', () => { + // TODO: Implement test }); - 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('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); - }); + it('throws when account not found', () => { + // TODO: Implement test }); + }); - 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); - }); - - 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); - }); + describe('setConfigAccountAsDefault()', () => { + it('sets account as default by id', () => { + // TODO: Implement test }); - 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); - }); - - 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(); - }); + it('sets account as default by name', () => { + // TODO: Implement test }); - describe('when a non-standard config is present', () => { - beforeAll(() => { - fsExistsSyncSpy.mockReturnValue(true); - (configFile.getConfigFilePath as jest.Mock).mockReturnValue( - CONFIG_PATHS.nonStandard - ); - }); - - 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); - }); + it('throws when account not found', () => { + // TODO: Implement test }); }); - 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; - }); - }); - - afterAll(() => { - setConfigPath(CONFIG_PATHS.default); - mockedConfigPath = CONFIG_PATHS.default; - fsExistsSyncSpy.mockRestore(); - }); - - it('writes a new config file', () => { - createEmptyConfigFile(); - - expect(fsWriteFileSyncSpy).toHaveBeenCalled(); - }); + describe('renameConfigAccount()', () => { + it('renames existing account', () => { + // TODO: Implement test }); - describe('when a config is present', () => { - let fsExistsSyncAndReturnTrueSpy: jest.SpyInstance; - - beforeAll(() => { - setConfigPath(CONFIG_PATHS.cwd); - mockedConfigPath = CONFIG_PATHS.cwd; - fsExistsSyncAndReturnTrueSpy = jest - .spyOn(fs, 'existsSync') - .mockImplementation(pathToCheck => { - if (pathToCheck === CONFIG_PATHS.cwd) { - return true; - } - - return false; - }); - }); - - afterAll(() => { - fsExistsSyncAndReturnTrueSpy.mockRestore(); - }); - - it('does nothing', () => { - createEmptyConfigFile(); - - expect(fsWriteFileSyncSpy).not.toHaveBeenCalled(); - }); + it('throws when account not found', () => { + // TODO: Implement test }); - describe('when passed a path', () => { - beforeAll(() => { - setConfigPath(CONFIG_PATHS.none); - mockedConfigPath = CONFIG_PATHS.none; - }); - - it('creates a config at the specified path', () => { - const specifiedPath = '/some/path/that/has/never/been/used.config.yml'; - createEmptyConfigFile({ path: specifiedPath }); - - expect(fsWriteFileSyncSpy).not.toHaveBeenCalledWith(specifiedPath); - }); + it('throws when new name already exists', () => { + // TODO: Implement test }); }); - describe('configFileExists', () => { - let getConfigPathSpy: jest.SpyInstance; - - beforeAll(() => { - getConfigPathSpy = jest.spyOn(config_DEPRECATED, 'getConfigPath'); + describe('removeAccountFromConfig()', () => { + it('removes existing account', () => { + // TODO: Implement test }); - beforeEach(() => { - jest.clearAllMocks(); + it('throws when account not found', () => { + // TODO: Implement test }); + }); - afterAll(() => { - getConfigPathSpy.mockRestore(); + describe('updateHttpTimeout()', () => { + it('updates timeout value', () => { + // TODO: Implement test }); - it('returns true when useHiddenConfig is true and newConfigFileExists returns true', () => { - (configFile.configFileExists as jest.Mock).mockReturnValue(true); - - const result = configFileExists(true); - - expect(configFile.configFileExists).toHaveBeenCalled(); - expect(result).toBe(true); + it('throws for invalid timeout', () => { + // TODO: Implement test }); + }); - it('returns false when useHiddenConfig is true and newConfigFileExists returns false', () => { - (configFile.configFileExists as jest.Mock).mockReturnValue(false); - - const result = configFileExists(true); - - expect(configFile.configFileExists).toHaveBeenCalled(); - expect(result).toBe(false); + describe('updateAllowUsageTracking()', () => { + it('updates tracking setting', () => { + // TODO: Implement test }); + }); - it('returns true when useHiddenConfig is false and config_DEPRECATED.getConfigPath returns a valid path', () => { - getConfigPathSpy.mockReturnValue(CONFIG_PATHS.default); - - const result = configFileExists(false); - - expect(getConfigPathSpy).toHaveBeenCalled(); - expect(result).toBe(true); + describe('updateDefaultCmsPublishMode()', () => { + it('updates publish mode', () => { + // TODO: Implement test }); - it('returns false when useHiddenConfig is false and config_DEPRECATED.getConfigPath returns an empty path', () => { - getConfigPathSpy.mockReturnValue(''); - - const result = configFileExists(false); - - expect(getConfigPathSpy).toHaveBeenCalled(); - expect(result).toBe(false); + it('throws for invalid mode', () => { + // TODO: Implement test }); + }); - it('defaults to useHiddenConfig as false when not provided', () => { - getConfigPathSpy.mockReturnValue(CONFIG_PATHS.default); - - const result = configFileExists(); + describe('isConfigFlagEnabled()', () => { + it('returns flag value when set', () => { + // TODO: Implement test + }); - expect(getConfigPathSpy).toHaveBeenCalled(); - expect(result).toBe(true); + it('returns default value when not set', () => { + // TODO: Implement test }); }); }); diff --git a/config/__tests__/config_old.test.ts b/config/__tests__/config_old.test.ts new file mode 100644 index 00000000..eca433d9 --- /dev/null +++ b/config/__tests__/config_old.test.ts @@ -0,0 +1,808 @@ +import fs from 'fs-extra'; +import { + setConfig, + getAndLoadConfigIfNeeded, + getConfig, + getAccountType, + getConfigPath, + getAccountConfig, + getAccountId, + updateDefaultAccount, + updateAccountConfig, + validateConfig, + deleteEmptyConfigFile, + setConfigPath, + createEmptyConfigFile, + configFileExists, +} 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 { + APIKeyAccount_DEPRECATED, + AuthType, + CLIAccount, + OAuthAccount, + OAuthAccount_DEPRECATED, + APIKeyAccount, + PersonalAccessKeyAccount, + PersonalAccessKeyAccount_DEPRECATED, +} 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', + auth: { + clientId: 'fakeClientId', + clientSecret: 'fakeClientSecret', + scopes: ['content'], + tokenInfo: { + expiresAt: '2020-01-01T00:00:00.000Z', + refreshToken: 'fakeOauthRefreshToken', + accessToken: 'fakeOauthAccessToken', + }, + }, + env: ENVIRONMENTS.QA, +}; + +const PERSONAL_ACCESS_KEY_CONFIG: PersonalAccessKeyAccount_DEPRECATED = { + name: 'PERSONALACCESSKEY', + authType: 'personalaccesskey', + auth: { + tokenInfo: { + expiresAt: '2020-01-01T00:00:00.000Z', + accessToken: 'fakePersonalAccessKeyAccessToken', + }, + }, + personalAccessKey: 'fakePersonalAccessKey', + env: ENVIRONMENTS.QA, + portalId: 1, +}; + +const PORTALS = [API_KEY_CONFIG, OAUTH2_CONFIG, PERSONAL_ACCESS_KEY_CONFIG]; + +const CONFIG: CLIConfig_DEPRECATED = { + defaultPortal: PORTALS[0].name, + portals: PORTALS, +}; + +function getAccountByAuthType( + config: CLIConfig | undefined | null, + authType: AuthType +): CLIAccount { + return getAccounts(config).filter(portal => portal.authType === authType)[0]; +} + +describe('config/config', () => { + const globalConsole = global.console; + beforeAll(() => { + global.console.error = jest.fn(); + global.console.debug = jest.fn(); + }); + afterAll(() => { + global.console = globalConsole; + }); + + describe('setConfig()', () => { + beforeEach(() => { + setConfig(CONFIG); + }); + + it('sets the config properly', () => { + expect(getConfig()).toEqual(CONFIG); + }); + }); + + describe('getAccountId()', () => { + beforeEach(() => { + process.env = {}; + setConfig({ + defaultPortal: PERSONAL_ACCESS_KEY_CONFIG.name, + portals: PORTALS, + }); + }); + + it('returns portalId from config when a name is passed', () => { + expect(getAccountId(OAUTH2_CONFIG.name)).toEqual(OAUTH2_CONFIG.portalId); + }); + + it('returns portalId from config when a string id is passed', () => { + expect(getAccountId((OAUTH2_CONFIG.portalId || '').toString())).toEqual( + OAUTH2_CONFIG.portalId + ); + }); + + it('returns portalId from config when a numeric id is passed', () => { + expect(getAccountId(OAUTH2_CONFIG.portalId)).toEqual( + OAUTH2_CONFIG.portalId + ); + }); + + 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 + ); + }); + }); + }); + + describe('updateDefaultAccount()', () => { + const myPortalName = 'Foo'; + + beforeEach(() => { + updateDefaultAccount(myPortalName); + }); + + it('sets the defaultPortal in the config', () => { + const config = getConfig(); + expect(config ? getDefaultAccount(config) : null).toEqual(myPortalName); + }); + }); + + 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(); + + deleteEmptyConfigFile(); + expect(fs.unlinkSync).not.toHaveBeenCalled(); + }); + + it('deletes config file if empty', () => { + jest.spyOn(fs, 'readFileSync').mockImplementation(() => ''); + jest.spyOn(fs, 'existsSync').mockImplementation(() => true); + fs.unlinkSync = jest.fn(); + + deleteEmptyConfigFile(); + expect(fs.unlinkSync).toHaveBeenCalled(); + }); + }); + + describe('updateAccountConfig()', () => { + const CONFIG = { + defaultPortal: PORTALS[0].name, + portals: PORTALS, + }; + + beforeEach(() => { + setConfig(CONFIG); + }); + + 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); + + expect( + getAccountByAuthType( + getConfig(), + modifiedPersonalAccessKeyConfig.authType + ).env + ).toEqual(newEnv); + }); + + 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); + + expect( + getAccountByAuthType( + getConfig(), + modifiedPersonalAccessKeyConfig.authType + ).name + ).toEqual(newName); + }); + }); + + describe('validateConfig()', () => { + const DEFAULT_PORTAL = PORTALS[0].name; + + it('allows valid config', () => { + setConfig({ + defaultPortal: DEFAULT_PORTAL, + portals: PORTALS, + }); + expect(validateConfig()).toEqual(true); + }); + + it('does not allow duplicate portalIds', () => { + setConfig({ + defaultPortal: DEFAULT_PORTAL, + portals: [...PORTALS, PORTALS[0]], + }); + expect(validateConfig()).toEqual(false); + }); + + it('does not allow duplicate names', () => { + setConfig({ + defaultPortal: DEFAULT_PORTAL, + portals: [ + ...PORTALS, + { + ...PORTALS[0], + portalId: 123456789, + }, + ], + }); + expect(validateConfig()).toEqual(false); + }); + + it('does not allow names with spaces', () => { + setConfig({ + defaultPortal: DEFAULT_PORTAL, + portals: [ + { + ...PORTALS[0], + name: 'A NAME WITH SPACES', + }, + ], + }); + expect(validateConfig()).toEqual(false); + }); + + it('allows multiple portals with no name', () => { + setConfig({ + defaultPortal: DEFAULT_PORTAL, + portals: [ + { + ...PORTALS[0], + name: undefined, + }, + { + ...PORTALS[1], + name: undefined, + }, + ], + }); + expect(validateConfig()).toEqual(true); + }); + }); + + 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(); + }); + + it('properly loads portal id value', () => { + expect(getAccountIdentifier(portalConfig)).toEqual(portalId); + }); + + it('properly loads client id value', () => { + expect(portalConfig?.auth.clientId).toEqual(clientId); + }); + + it('properly loads client secret value', () => { + expect(portalConfig?.auth.clientSecret).toEqual(clientSecret); + }); + + it('properly loads refresh token value', () => { + expect(portalConfig?.auth?.tokenInfo?.refreshToken).toEqual( + refreshToken + ); + }); + }); + + 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(); + }); + + 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(); + }); + + it('properly loads portal id value', () => { + expect(getAccountIdentifier(portalConfig)).toEqual(portalId); + }); + + it('properly loads api key value', () => { + expect(portalConfig.apiKey).toEqual(apiKey); + }); + }); + + 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(); + }); + + 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(); + }); + + it('properly loads portal id value', () => { + expect(getAccountIdentifier(portalConfig)).toEqual(portalId); + }); + + it('properly loads personal access key value', () => { + expect(portalConfig?.personalAccessKey).toEqual(personalAccessKey); + }); + }); + }); + + 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 + ); + }); + 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; + }); + }); + + 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('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); + }); + }); + + 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); + }); + + 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); + }); + }); + + 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); + }); + + 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(); + }); + }); + + describe('when a non-standard config is present', () => { + beforeAll(() => { + fsExistsSyncSpy.mockReturnValue(true); + (configFile.getConfigFilePath as jest.Mock).mockReturnValue( + CONFIG_PATHS.nonStandard + ); + }); + + 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); + }); + }); + }); + + 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; + }); + }); + + afterAll(() => { + setConfigPath(CONFIG_PATHS.default); + mockedConfigPath = CONFIG_PATHS.default; + fsExistsSyncSpy.mockRestore(); + }); + + it('writes a new config file', () => { + createEmptyConfigFile(); + + expect(fsWriteFileSyncSpy).toHaveBeenCalled(); + }); + }); + + describe('when a config is present', () => { + let fsExistsSyncAndReturnTrueSpy: jest.SpyInstance; + + beforeAll(() => { + setConfigPath(CONFIG_PATHS.cwd); + mockedConfigPath = CONFIG_PATHS.cwd; + fsExistsSyncAndReturnTrueSpy = jest + .spyOn(fs, 'existsSync') + .mockImplementation(pathToCheck => { + if (pathToCheck === CONFIG_PATHS.cwd) { + return true; + } + + return false; + }); + }); + + afterAll(() => { + fsExistsSyncAndReturnTrueSpy.mockRestore(); + }); + + it('does nothing', () => { + createEmptyConfigFile(); + + expect(fsWriteFileSyncSpy).not.toHaveBeenCalled(); + }); + }); + + describe('when passed a path', () => { + beforeAll(() => { + setConfigPath(CONFIG_PATHS.none); + mockedConfigPath = CONFIG_PATHS.none; + }); + + it('creates a config at the specified path', () => { + const specifiedPath = '/some/path/that/has/never/been/used.config.yml'; + createEmptyConfigFile({ path: specifiedPath }); + + expect(fsWriteFileSyncSpy).not.toHaveBeenCalledWith(specifiedPath); + }); + }); + }); + + describe('configFileExists', () => { + let getConfigPathSpy: jest.SpyInstance; + + beforeAll(() => { + getConfigPathSpy = jest.spyOn(config_DEPRECATED, 'getConfigPath'); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterAll(() => { + getConfigPathSpy.mockRestore(); + }); + + it('returns true when useHiddenConfig is true and newConfigFileExists returns true', () => { + (configFile.configFileExists as jest.Mock).mockReturnValue(true); + + const result = configFileExists(true); + + expect(configFile.configFileExists).toHaveBeenCalled(); + expect(result).toBe(true); + }); + + it('returns false when useHiddenConfig is true and newConfigFileExists returns false', () => { + (configFile.configFileExists as jest.Mock).mockReturnValue(false); + + const result = configFileExists(true); + + expect(configFile.configFileExists).toHaveBeenCalled(); + expect(result).toBe(false); + }); + + it('returns true when useHiddenConfig is false and config_DEPRECATED.getConfigPath returns a valid path', () => { + getConfigPathSpy.mockReturnValue(CONFIG_PATHS.default); + + const result = configFileExists(false); + + expect(getConfigPathSpy).toHaveBeenCalled(); + expect(result).toBe(true); + }); + + it('returns false when useHiddenConfig is false and config_DEPRECATED.getConfigPath returns an empty path', () => { + getConfigPathSpy.mockReturnValue(''); + + const result = configFileExists(false); + + expect(getConfigPathSpy).toHaveBeenCalled(); + expect(result).toBe(false); + }); + + it('defaults to useHiddenConfig as false when not provided', () => { + getConfigPathSpy.mockReturnValue(CONFIG_PATHS.default); + + const result = configFileExists(); + + expect(getConfigPathSpy).toHaveBeenCalled(); + expect(result).toBe(true); + }); + }); +}); diff --git a/config/__tests__/utils.test.ts b/config/__tests__/utils.test.ts index 49661fa3..bebfe20a 100644 --- a/config/__tests__/utils.test.ts +++ b/config/__tests__/utils.test.ts @@ -122,6 +122,14 @@ function cleanupEnvironmentVariables() { } describe('config/utils', () => { + beforeEach(() => { + cleanupEnvironmentVariables(); + }); + + afterEach(() => { + cleanupEnvironmentVariables(); + }); + describe('getGlobalConfigFilePath()', () => { it('returns the global config file path', () => { const globalConfigFilePath = getGlobalConfigFilePath(); @@ -164,7 +172,6 @@ describe('config/utils', () => { const result = getConfigPathEnvironmentVariables(); expect(result.useEnvironmentConfig).toBe(false); expect(result.configFilePathFromEnvironment).toBe(configPath); - cleanupEnvironmentVariables(); }); it('throws when both environment variables are set', () => { @@ -172,7 +179,6 @@ describe('config/utils', () => { process.env[ENVIRONMENT_VARIABLES.USE_ENVIRONMENT_CONFIG] = 'true'; expect(() => getConfigPathEnvironmentVariables()).toThrow(); - cleanupEnvironmentVariables(); }); }); @@ -260,7 +266,6 @@ describe('config/utils', () => { ...CONFIG, accounts: [{ ...PAK_ACCOUNT, name: '123', accountType: undefined }], }); - cleanupEnvironmentVariables(); }); it('builds OAuth config', () => { @@ -282,7 +287,6 @@ describe('config/utils', () => { ...CONFIG, accounts: [OAUTH_ACCOUNT], }); - cleanupEnvironmentVariables(); }); it('throws when required variables missing', () => { @@ -291,8 +295,6 @@ describe('config/utils', () => { process.env[ENVIRONMENT_VARIABLES.HUBSPOT_ENVIRONMENT] = 'qa'; buildConfigFromEnvironment(); }).toThrow(); - - cleanupEnvironmentVariables(); }); }); From 5cb2fa026f64367d3cb64952b7444eef0c88d226 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Tue, 11 Feb 2025 13:18:22 -0500 Subject: [PATCH 19/70] Finish tests for config --- config/__tests__/config.test.ts | 282 ++++++++++++++++++++++++-------- config/index.ts | 8 +- config/utils.ts | 2 +- 3 files changed, 225 insertions(+), 67 deletions(-) diff --git a/config/__tests__/config.test.ts b/config/__tests__/config.test.ts index 1f3f6a9b..7edfe81a 100644 --- a/config/__tests__/config.test.ts +++ b/config/__tests__/config.test.ts @@ -1,5 +1,6 @@ import findup from 'findup-sync'; import fs from 'fs-extra'; +import yaml from 'js-yaml'; import { localConfigFileExists, @@ -26,7 +27,6 @@ import { } from '../index'; import { HubSpotConfigAccount } from '../../types/Accounts'; import { HubSpotConfig } from '../../types/Config'; -import { getCwd } from '../../lib/path'; import { PersonalAccessKeyConfigAccount, OAuthConfigAccount, @@ -40,16 +40,16 @@ import { import { getGlobalConfigFilePath, getLocalConfigFileDefaultPath, + formatConfigForWrite, } from '../utils'; -import { ENVIRONMENT_VARIABLES } from '../../constants/config'; +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'); const mockFindup = findup as jest.MockedFunction; -const mockCwd = getCwd as jest.MockedFunction; const mockFs = fs as jest.Mocked; const PAK_ACCOUNT: PersonalAccessKeyConfigAccount = { @@ -65,9 +65,9 @@ const PAK_ACCOUNT: PersonalAccessKeyConfigAccount = { }; const OAUTH_ACCOUNT: OAuthConfigAccount = { - accountId: 123, + accountId: 234, env: 'qa', - name: '123', + name: '234', authType: OAUTH_AUTH_METHOD.value, accountType: undefined, auth: { @@ -81,12 +81,12 @@ const OAUTH_ACCOUNT: OAuthConfigAccount = { }; const API_KEY_ACCOUNT: APIKeyConfigAccount = { - accountId: 123, + accountId: 345, env: 'qa', - name: '123', + name: 'api-key-account', authType: API_KEY_AUTH_METHOD.value, - accountType: undefined, apiKey: 'test-api-key', + accountType: 'STANDARD', }; const CONFIG: HubSpotConfig = { @@ -98,19 +98,27 @@ const CONFIG: HubSpotConfig = { allowUsageTracking: true, }; -function cleanupEnvironmentVariables() { +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/index', () => { - beforeEach(() => { - cleanupEnvironmentVariables(); - }); +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(() => { - cleanupEnvironmentVariables(); + cleanup(); }); describe('localConfigFileExists()', () => { @@ -177,37 +185,26 @@ describe('config/index', () => { }); it('returns parsed config from file', () => { - mockFs.existsSync.mockReturnValueOnce(true); - mockFs.readFileSync.mockReturnValueOnce('test-config-content'); - jest.spyOn(utils, 'parseConfig').mockReturnValueOnce(CONFIG); - + mockConfig(); expect(getConfig()).toEqual(CONFIG); }); }); describe('isConfigValid()', () => { it('returns true for valid config', () => { - mockFs.existsSync.mockReturnValueOnce(true); - mockFs.readFileSync.mockReturnValueOnce('test-config-content'); - jest.spyOn(utils, 'parseConfig').mockReturnValueOnce(CONFIG); + mockConfig(); expect(isConfigValid()).toBe(true); }); it('returns false for config with no accounts', () => { - mockFs.existsSync.mockReturnValueOnce(true); - mockFs.readFileSync.mockReturnValueOnce('test-config-content'); - jest.spyOn(utils, 'parseConfig').mockReturnValueOnce({ accounts: [] }); + mockConfig({ accounts: [] }); expect(isConfigValid()).toBe(false); }); it('returns false for config with duplicate account ids', () => { - mockFs.existsSync.mockReturnValueOnce(true); - mockFs.readFileSync.mockReturnValueOnce('test-config-content'); - jest - .spyOn(utils, 'parseConfig') - .mockReturnValueOnce({ accounts: [PAK_ACCOUNT, PAK_ACCOUNT] }); + mockConfig({ accounts: [PAK_ACCOUNT, PAK_ACCOUNT] }); expect(isConfigValid()).toBe(false); }); @@ -215,163 +212,320 @@ describe('config/index', () => { describe('createEmptyConfigFile()', () => { it('creates global config when specified', () => { - // TODO: Implement test + mockFs.existsSync.mockReturnValueOnce(true); + createEmptyConfigFile(true); + + expect(mockFs.writeFileSync).toHaveBeenCalledWith( + getGlobalConfigFilePath(), + yaml.dump({ accounts: [] }) + ); }); it('creates local config by default', () => { - // TODO: Implement test + mockFs.existsSync.mockReturnValueOnce(true); + createEmptyConfigFile(false); + + expect(mockFs.writeFileSync).toHaveBeenCalledWith( + getLocalConfigFileDefaultPath(), + yaml.dump({ accounts: [] }) + ); }); }); describe('deleteConfigFile()', () => { - it('deletes the config file', () => {}); + it('deletes the config file', () => { + mockFs.existsSync.mockReturnValue(true); + deleteConfigFile(); + + expect(mockFs.unlinkSync).toHaveBeenCalledWith(getConfigFilePath()); + }); }); describe('getConfigAccountById()', () => { it('returns account when found', () => { - // TODO: Implement test + mockConfig(); + + expect(getConfigAccountById(123)).toEqual(PAK_ACCOUNT); }); it('throws when account not found', () => { - // TODO: Implement test + mockConfig(); + + expect(() => getConfigAccountById(456)).toThrow(); }); }); describe('getConfigAccountByName()', () => { it('returns account when found', () => { - // TODO: Implement test + mockConfig(); + + expect(getConfigAccountByName('test-account')).toEqual(PAK_ACCOUNT); }); it('throws when account not found', () => { - // TODO: Implement test + mockConfig(); + + expect(() => getConfigAccountByName('non-existent-account')).toThrow(); }); }); describe('getConfigDefaultAccount()', () => { it('returns default account when set', () => { - // TODO: Implement test + mockConfig(); + + expect(getConfigDefaultAccount()).toEqual(PAK_ACCOUNT); }); it('throws when no default account', () => { - // TODO: Implement test + mockConfig({ accounts: [] }); + + expect(() => getConfigDefaultAccount()).toThrow(); }); }); describe('getAllConfigAccounts()', () => { it('returns all accounts', () => { - // TODO: Implement test + mockConfig(); + + expect(getAllConfigAccounts()).toEqual([PAK_ACCOUNT]); }); }); describe('getConfigAccountEnvironment()', () => { it('returns environment for specified account', () => { - // TODO: Implement test + mockConfig(); + + expect(getConfigAccountEnvironment(123)).toEqual('qa'); }); it('returns default account environment when no identifier', () => { - // TODO: Implement test + mockConfig(); + + expect(getConfigAccountEnvironment()).toEqual('qa'); }); }); describe('addConfigAccount()', () => { it('adds valid account to config', () => { - // TODO: Implement test + mockConfig(); + // eslint-disable-next-line @typescript-eslint/no-empty-function + mockFs.writeFileSync.mockImplementationOnce(() => {}); + addConfigAccount(OAUTH_ACCOUNT); + + expect(mockFs.writeFileSync).toHaveBeenCalledWith( + getConfigFilePath(), + yaml.dump( + formatConfigForWrite({ + ...CONFIG, + accounts: [PAK_ACCOUNT, OAUTH_ACCOUNT], + }) + ) + ); }); it('throws for invalid account', () => { - // TODO: Implement test + expect(() => + addConfigAccount({ + ...PAK_ACCOUNT, + personalAccessKey: null, + } as unknown as HubSpotConfigAccount) + ).toThrow(); }); it('throws when account already exists', () => { - // TODO: Implement test + mockConfig(); + + expect(() => addConfigAccount(PAK_ACCOUNT)).toThrow(); }); }); describe('updateConfigAccount()', () => { it('updates existing account', () => { - // TODO: Implement test - }); + mockConfig(); + const newAccount = { ...PAK_ACCOUNT, name: 'new-name' }; + + updateConfigAccount(newAccount); + + expect(mockFs.writeFileSync).toHaveBeenCalledWith( + getConfigFilePath(), + yaml.dump(formatConfigForWrite({ ...CONFIG, accounts: [newAccount] })) + ); + }); it('throws for invalid account', () => { - // TODO: Implement test + expect(() => + updateConfigAccount({ + ...PAK_ACCOUNT, + personalAccessKey: null, + } as unknown as HubSpotConfigAccount) + ).toThrow(); }); it('throws when account not found', () => { - // TODO: Implement test + mockConfig(); + + expect(() => updateConfigAccount(OAUTH_ACCOUNT)).toThrow(); }); }); describe('setConfigAccountAsDefault()', () => { it('sets account as default by id', () => { - // TODO: Implement test + const config = { ...CONFIG, accounts: [PAK_ACCOUNT, API_KEY_ACCOUNT] }; + mockConfig(config); + + setConfigAccountAsDefault(345); + + expect(mockFs.writeFileSync).toHaveBeenCalledWith( + getConfigFilePath(), + yaml.dump(formatConfigForWrite({ ...config, defaultAccount: 345 })) + ); }); it('sets account as default by name', () => { - // TODO: Implement test + const config = { ...CONFIG, accounts: [PAK_ACCOUNT, API_KEY_ACCOUNT] }; + mockConfig(config); + + setConfigAccountAsDefault('api-key-account'); + + expect(mockFs.writeFileSync).toHaveBeenCalledWith( + getConfigFilePath(), + yaml.dump(formatConfigForWrite({ ...config, defaultAccount: 345 })) + ); }); it('throws when account not found', () => { - // TODO: Implement test + expect(() => setConfigAccountAsDefault('non-existent-account')).toThrow(); }); }); describe('renameConfigAccount()', () => { it('renames existing account', () => { - // TODO: Implement test + mockConfig(); + + renameConfigAccount('test-account', 'new-name'); + + expect(mockFs.writeFileSync).toHaveBeenCalledWith( + getConfigFilePath(), + yaml.dump( + formatConfigForWrite({ + ...CONFIG, + accounts: [{ ...PAK_ACCOUNT, name: 'new-name' }], + }) + ) + ); }); it('throws when account not found', () => { - // TODO: Implement test + expect(() => + renameConfigAccount('non-existent-account', 'new-name') + ).toThrow(); }); it('throws when new name already exists', () => { - // TODO: Implement test + const config = { ...CONFIG, accounts: [PAK_ACCOUNT, API_KEY_ACCOUNT] }; + mockConfig(config); + + expect(() => + renameConfigAccount('test-account', 'api-key-account') + ).toThrow(); }); }); describe('removeAccountFromConfig()', () => { it('removes existing account', () => { - // TODO: Implement test + mockConfig(); + + removeAccountFromConfig(123); + + expect(mockFs.writeFileSync).toHaveBeenCalledWith( + getConfigFilePath(), + yaml.dump( + formatConfigForWrite({ + ...CONFIG, + accounts: [], + defaultAccount: undefined, + }) + ) + ); }); it('throws when account not found', () => { - // TODO: Implement test + mockConfig(); + + expect(() => removeAccountFromConfig(456)).toThrow(); }); }); describe('updateHttpTimeout()', () => { it('updates timeout value', () => { - // TODO: Implement test + mockConfig(); + + updateHttpTimeout(4000); + + expect(mockFs.writeFileSync).toHaveBeenCalledWith( + getConfigFilePath(), + yaml.dump(formatConfigForWrite({ ...CONFIG, httpTimeout: 4000 })) + ); }); it('throws for invalid timeout', () => { - // TODO: Implement test + expect(() => updateHttpTimeout('invalid-timeout')).toThrow(); }); }); describe('updateAllowUsageTracking()', () => { it('updates tracking setting', () => { - // TODO: Implement test + mockConfig(); + updateAllowUsageTracking(false); + + expect(mockFs.writeFileSync).toHaveBeenCalledWith( + getConfigFilePath(), + yaml.dump( + formatConfigForWrite({ ...CONFIG, allowUsageTracking: false }) + ) + ); }); }); describe('updateDefaultCmsPublishMode()', () => { it('updates publish mode', () => { - // TODO: Implement test + mockConfig(); + + updateDefaultCmsPublishMode('draft'); + + expect(mockFs.writeFileSync).toHaveBeenCalledWith( + getConfigFilePath(), + yaml.dump( + formatConfigForWrite({ ...CONFIG, defaultCmsPublishMode: 'draft' }) + ) + ); }); it('throws for invalid mode', () => { - // TODO: Implement test + expect(() => + updateDefaultCmsPublishMode('invalid-mode' as unknown as CmsPublishMode) + ).toThrow(); }); }); describe('isConfigFlagEnabled()', () => { it('returns flag value when set', () => { - // TODO: Implement test + mockConfig({ + ...CONFIG, + [CONFIG_FLAGS.USE_CUSTOM_OBJECT_HUBFILE]: true, + }); + + expect(isConfigFlagEnabled(CONFIG_FLAGS.USE_CUSTOM_OBJECT_HUBFILE)).toBe( + true + ); }); it('returns default value when not set', () => { - // TODO: Implement test + mockConfig(); + + expect( + isConfigFlagEnabled(CONFIG_FLAGS.USE_CUSTOM_OBJECT_HUBFILE, true) + ).toBe(true); }); }); }); diff --git a/config/index.ts b/config/index.ts index c1eeda41..9039a890 100644 --- a/config/index.ts +++ b/config/index.ts @@ -299,6 +299,10 @@ export function removeAccountFromConfig(accountId: number): void { config.accounts.splice(index, 1); + if (config.defaultAccount === accountId) { + delete config.defaultAccount; + } + writeConfigFile(config, getConfigFilePath()); } @@ -344,12 +348,12 @@ export function updateDefaultCmsPublishMode( export function isConfigFlagEnabled( flag: ConfigFlag, - defaultValue: boolean + defaultValue?: boolean ): boolean { const config = getConfig(); if (typeof config[flag] === 'undefined') { - return defaultValue; + return defaultValue || false; } return Boolean(config[flag]); diff --git a/config/utils.ts b/config/utils.ts index eff28d88..2c8a8340 100644 --- a/config/utils.ts +++ b/config/utils.ts @@ -126,7 +126,7 @@ export function removeUndefinedFieldsFromConfigAccount< } // Ensure written config files have fields in a consistent order -function formatConfigForWrite(config: HubSpotConfig) { +export function formatConfigForWrite(config: HubSpotConfig) { const { defaultAccount, defaultCmsPublishMode, From e85f998608140b7002c32eb55b83283d3538e32c Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Tue, 11 Feb 2025 13:18:40 -0500 Subject: [PATCH 20/70] Delete old tests --- config/__tests__/config_old.test.ts | 808 ---------------------------- 1 file changed, 808 deletions(-) delete mode 100644 config/__tests__/config_old.test.ts diff --git a/config/__tests__/config_old.test.ts b/config/__tests__/config_old.test.ts deleted file mode 100644 index eca433d9..00000000 --- a/config/__tests__/config_old.test.ts +++ /dev/null @@ -1,808 +0,0 @@ -import fs from 'fs-extra'; -import { - setConfig, - getAndLoadConfigIfNeeded, - getConfig, - getAccountType, - getConfigPath, - getAccountConfig, - getAccountId, - updateDefaultAccount, - updateAccountConfig, - validateConfig, - deleteEmptyConfigFile, - setConfigPath, - createEmptyConfigFile, - configFileExists, -} 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 { - APIKeyAccount_DEPRECATED, - AuthType, - CLIAccount, - OAuthAccount, - OAuthAccount_DEPRECATED, - APIKeyAccount, - PersonalAccessKeyAccount, - PersonalAccessKeyAccount_DEPRECATED, -} 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', - auth: { - clientId: 'fakeClientId', - clientSecret: 'fakeClientSecret', - scopes: ['content'], - tokenInfo: { - expiresAt: '2020-01-01T00:00:00.000Z', - refreshToken: 'fakeOauthRefreshToken', - accessToken: 'fakeOauthAccessToken', - }, - }, - env: ENVIRONMENTS.QA, -}; - -const PERSONAL_ACCESS_KEY_CONFIG: PersonalAccessKeyAccount_DEPRECATED = { - name: 'PERSONALACCESSKEY', - authType: 'personalaccesskey', - auth: { - tokenInfo: { - expiresAt: '2020-01-01T00:00:00.000Z', - accessToken: 'fakePersonalAccessKeyAccessToken', - }, - }, - personalAccessKey: 'fakePersonalAccessKey', - env: ENVIRONMENTS.QA, - portalId: 1, -}; - -const PORTALS = [API_KEY_CONFIG, OAUTH2_CONFIG, PERSONAL_ACCESS_KEY_CONFIG]; - -const CONFIG: CLIConfig_DEPRECATED = { - defaultPortal: PORTALS[0].name, - portals: PORTALS, -}; - -function getAccountByAuthType( - config: CLIConfig | undefined | null, - authType: AuthType -): CLIAccount { - return getAccounts(config).filter(portal => portal.authType === authType)[0]; -} - -describe('config/config', () => { - const globalConsole = global.console; - beforeAll(() => { - global.console.error = jest.fn(); - global.console.debug = jest.fn(); - }); - afterAll(() => { - global.console = globalConsole; - }); - - describe('setConfig()', () => { - beforeEach(() => { - setConfig(CONFIG); - }); - - it('sets the config properly', () => { - expect(getConfig()).toEqual(CONFIG); - }); - }); - - describe('getAccountId()', () => { - beforeEach(() => { - process.env = {}; - setConfig({ - defaultPortal: PERSONAL_ACCESS_KEY_CONFIG.name, - portals: PORTALS, - }); - }); - - it('returns portalId from config when a name is passed', () => { - expect(getAccountId(OAUTH2_CONFIG.name)).toEqual(OAUTH2_CONFIG.portalId); - }); - - it('returns portalId from config when a string id is passed', () => { - expect(getAccountId((OAUTH2_CONFIG.portalId || '').toString())).toEqual( - OAUTH2_CONFIG.portalId - ); - }); - - it('returns portalId from config when a numeric id is passed', () => { - expect(getAccountId(OAUTH2_CONFIG.portalId)).toEqual( - OAUTH2_CONFIG.portalId - ); - }); - - 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 - ); - }); - }); - }); - - describe('updateDefaultAccount()', () => { - const myPortalName = 'Foo'; - - beforeEach(() => { - updateDefaultAccount(myPortalName); - }); - - it('sets the defaultPortal in the config', () => { - const config = getConfig(); - expect(config ? getDefaultAccount(config) : null).toEqual(myPortalName); - }); - }); - - 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(); - - deleteEmptyConfigFile(); - expect(fs.unlinkSync).not.toHaveBeenCalled(); - }); - - it('deletes config file if empty', () => { - jest.spyOn(fs, 'readFileSync').mockImplementation(() => ''); - jest.spyOn(fs, 'existsSync').mockImplementation(() => true); - fs.unlinkSync = jest.fn(); - - deleteEmptyConfigFile(); - expect(fs.unlinkSync).toHaveBeenCalled(); - }); - }); - - describe('updateAccountConfig()', () => { - const CONFIG = { - defaultPortal: PORTALS[0].name, - portals: PORTALS, - }; - - beforeEach(() => { - setConfig(CONFIG); - }); - - 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); - - expect( - getAccountByAuthType( - getConfig(), - modifiedPersonalAccessKeyConfig.authType - ).env - ).toEqual(newEnv); - }); - - 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); - - expect( - getAccountByAuthType( - getConfig(), - modifiedPersonalAccessKeyConfig.authType - ).name - ).toEqual(newName); - }); - }); - - describe('validateConfig()', () => { - const DEFAULT_PORTAL = PORTALS[0].name; - - it('allows valid config', () => { - setConfig({ - defaultPortal: DEFAULT_PORTAL, - portals: PORTALS, - }); - expect(validateConfig()).toEqual(true); - }); - - it('does not allow duplicate portalIds', () => { - setConfig({ - defaultPortal: DEFAULT_PORTAL, - portals: [...PORTALS, PORTALS[0]], - }); - expect(validateConfig()).toEqual(false); - }); - - it('does not allow duplicate names', () => { - setConfig({ - defaultPortal: DEFAULT_PORTAL, - portals: [ - ...PORTALS, - { - ...PORTALS[0], - portalId: 123456789, - }, - ], - }); - expect(validateConfig()).toEqual(false); - }); - - it('does not allow names with spaces', () => { - setConfig({ - defaultPortal: DEFAULT_PORTAL, - portals: [ - { - ...PORTALS[0], - name: 'A NAME WITH SPACES', - }, - ], - }); - expect(validateConfig()).toEqual(false); - }); - - it('allows multiple portals with no name', () => { - setConfig({ - defaultPortal: DEFAULT_PORTAL, - portals: [ - { - ...PORTALS[0], - name: undefined, - }, - { - ...PORTALS[1], - name: undefined, - }, - ], - }); - expect(validateConfig()).toEqual(true); - }); - }); - - 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(); - }); - - it('properly loads portal id value', () => { - expect(getAccountIdentifier(portalConfig)).toEqual(portalId); - }); - - it('properly loads client id value', () => { - expect(portalConfig?.auth.clientId).toEqual(clientId); - }); - - it('properly loads client secret value', () => { - expect(portalConfig?.auth.clientSecret).toEqual(clientSecret); - }); - - it('properly loads refresh token value', () => { - expect(portalConfig?.auth?.tokenInfo?.refreshToken).toEqual( - refreshToken - ); - }); - }); - - 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(); - }); - - 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(); - }); - - it('properly loads portal id value', () => { - expect(getAccountIdentifier(portalConfig)).toEqual(portalId); - }); - - it('properly loads api key value', () => { - expect(portalConfig.apiKey).toEqual(apiKey); - }); - }); - - 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(); - }); - - 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(); - }); - - it('properly loads portal id value', () => { - expect(getAccountIdentifier(portalConfig)).toEqual(portalId); - }); - - it('properly loads personal access key value', () => { - expect(portalConfig?.personalAccessKey).toEqual(personalAccessKey); - }); - }); - }); - - 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 - ); - }); - 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; - }); - }); - - 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('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); - }); - }); - - 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); - }); - - 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); - }); - }); - - 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); - }); - - 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(); - }); - }); - - describe('when a non-standard config is present', () => { - beforeAll(() => { - fsExistsSyncSpy.mockReturnValue(true); - (configFile.getConfigFilePath as jest.Mock).mockReturnValue( - CONFIG_PATHS.nonStandard - ); - }); - - 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); - }); - }); - }); - - 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; - }); - }); - - afterAll(() => { - setConfigPath(CONFIG_PATHS.default); - mockedConfigPath = CONFIG_PATHS.default; - fsExistsSyncSpy.mockRestore(); - }); - - it('writes a new config file', () => { - createEmptyConfigFile(); - - expect(fsWriteFileSyncSpy).toHaveBeenCalled(); - }); - }); - - describe('when a config is present', () => { - let fsExistsSyncAndReturnTrueSpy: jest.SpyInstance; - - beforeAll(() => { - setConfigPath(CONFIG_PATHS.cwd); - mockedConfigPath = CONFIG_PATHS.cwd; - fsExistsSyncAndReturnTrueSpy = jest - .spyOn(fs, 'existsSync') - .mockImplementation(pathToCheck => { - if (pathToCheck === CONFIG_PATHS.cwd) { - return true; - } - - return false; - }); - }); - - afterAll(() => { - fsExistsSyncAndReturnTrueSpy.mockRestore(); - }); - - it('does nothing', () => { - createEmptyConfigFile(); - - expect(fsWriteFileSyncSpy).not.toHaveBeenCalled(); - }); - }); - - describe('when passed a path', () => { - beforeAll(() => { - setConfigPath(CONFIG_PATHS.none); - mockedConfigPath = CONFIG_PATHS.none; - }); - - it('creates a config at the specified path', () => { - const specifiedPath = '/some/path/that/has/never/been/used.config.yml'; - createEmptyConfigFile({ path: specifiedPath }); - - expect(fsWriteFileSyncSpy).not.toHaveBeenCalledWith(specifiedPath); - }); - }); - }); - - describe('configFileExists', () => { - let getConfigPathSpy: jest.SpyInstance; - - beforeAll(() => { - getConfigPathSpy = jest.spyOn(config_DEPRECATED, 'getConfigPath'); - }); - - beforeEach(() => { - jest.clearAllMocks(); - }); - - afterAll(() => { - getConfigPathSpy.mockRestore(); - }); - - it('returns true when useHiddenConfig is true and newConfigFileExists returns true', () => { - (configFile.configFileExists as jest.Mock).mockReturnValue(true); - - const result = configFileExists(true); - - expect(configFile.configFileExists).toHaveBeenCalled(); - expect(result).toBe(true); - }); - - it('returns false when useHiddenConfig is true and newConfigFileExists returns false', () => { - (configFile.configFileExists as jest.Mock).mockReturnValue(false); - - const result = configFileExists(true); - - expect(configFile.configFileExists).toHaveBeenCalled(); - expect(result).toBe(false); - }); - - it('returns true when useHiddenConfig is false and config_DEPRECATED.getConfigPath returns a valid path', () => { - getConfigPathSpy.mockReturnValue(CONFIG_PATHS.default); - - const result = configFileExists(false); - - expect(getConfigPathSpy).toHaveBeenCalled(); - expect(result).toBe(true); - }); - - it('returns false when useHiddenConfig is false and config_DEPRECATED.getConfigPath returns an empty path', () => { - getConfigPathSpy.mockReturnValue(''); - - const result = configFileExists(false); - - expect(getConfigPathSpy).toHaveBeenCalled(); - expect(result).toBe(false); - }); - - it('defaults to useHiddenConfig as false when not provided', () => { - getConfigPathSpy.mockReturnValue(CONFIG_PATHS.default); - - const result = configFileExists(); - - expect(getConfigPathSpy).toHaveBeenCalled(); - expect(result).toBe(true); - }); - }); -}); From 6ec40b365f623c6f032c93c9d5e7e13eb02f126f Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Tue, 11 Feb 2025 14:03:32 -0500 Subject: [PATCH 21/70] update PAK tests --- lib/__tests__/personalAccessKey.test.ts | 96 ++++++++++++++----------- 1 file changed, 54 insertions(+), 42 deletions(-) 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, From 59152d2ebe20ffb7f50c1bf6f1e491899363fa96 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Tue, 11 Feb 2025 17:06:34 -0500 Subject: [PATCH 22/70] Fix existing tests --- http/__tests__/getAxiosConfig.test.ts | 11 ++-- http/__tests__/index.test.ts | 79 +++++++++++--------------- http/getAxiosConfig.ts | 8 ++- lib/__tests__/environment.test.ts | 1 - lib/__tests__/oauth.test.ts | 62 ++++++++++---------- lib/__tests__/themes.test.ts | 15 +++-- lib/__tests__/trackUsage.test.ts | 30 +++++----- models/__tests__/OAuth2Manager.test.ts | 20 +++---- 8 files changed, 106 insertions(+), 120 deletions(-) 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 c66bb330..856045c1 100644 --- a/http/getAxiosConfig.ts +++ b/http/getAxiosConfig.ts @@ -2,6 +2,7 @@ import { version } from '../package.json'; 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 = getConfig(); + let config: HubSpotConfig | null; + try { + config = getConfig(); + } catch (e) { + config = null; + } let httpTimeout = 15000; let httpUseLocalhost = false; 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__/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/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 ); }); From c0cdc177c3011499b573bc521aa50ed8e25cf98a Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Tue, 11 Feb 2025 17:50:07 -0500 Subject: [PATCH 23/70] Add error and debug copy to new config methods --- config/README.md | 21 +++--- config/index.ts | 92 +++++++++++++++++++------- config/utils.ts | 66 +++++++++++++++---- http/index.ts | 7 +- lang/en.json | 136 ++++++++++++++++++--------------------- lib/personalAccessKey.ts | 12 +++- types/Accounts.ts | 2 +- 7 files changed, 211 insertions(+), 125 deletions(-) diff --git a/config/README.md b/config/README.md index 1dc6a724..017212fc 100644 --- a/config/README.md +++ b/config/README.md @@ -1,5 +1,3 @@ -TODO: UPDATE THIS - # hubspot/local-dev-lib ## Config utils @@ -12,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 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 -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. +- `USE_ENVIRONTMENT_CONFIG` - load config account from environment variables +- `HUBSPOT_CONFIG_PATH` - specify a path to a specific config file -#### updateAccountConfig() +#### updateConfigAccount() -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. +Safely writes updated values to the `hubspot.config.yml` file. -#### getAccountConfig() +#### getConfigAccountById() and getConfigAccountByName() -Returns config data for a specific account, given the account's ID. +Returns config data for a specific account, given the account's ID or name. ## Example config @@ -41,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/index.ts b/config/index.ts index 9039a890..fa787633 100644 --- a/config/index.ts +++ b/config/index.ts @@ -21,7 +21,7 @@ import { } from './utils'; import { CMS_PUBLISH_MODE } from '../constants/files'; import { Environment } from '../types/Config'; - +import { i18n } from '../utils/lang'; export function localConfigFileExists(): boolean { return Boolean(getLocalConfigFilePath()); } @@ -40,7 +40,7 @@ function getDefaultConfigFilePath(): string { const localConfigFilePath = getLocalConfigFilePath(); if (!localConfigFilePath) { - throw new Error('@TODO'); + throw new Error(i18n('config.getDefaultConfigFilePath.error')); } return localConfigFilePath; @@ -61,7 +61,7 @@ export function getConfig(): HubSpotConfig { const pathToRead = getConfigFilePath(); - logger.debug(`@TODOReading config from ${pathToRead}`); + logger.debug(i18n('config.getConfig', { path: pathToRead })); const configFileSource = readConfigFile(pathToRead); return parseConfig(configFileSource); @@ -71,7 +71,7 @@ export function isConfigValid(): boolean { const config = getConfig(); if (config.accounts.length === 0) { - logger.log('@TODO'); + logger.debug(i18n('config.isConfigValid.missingAccounts')); return false; } @@ -80,20 +80,31 @@ export function isConfigValid(): boolean { return config.accounts.every(account => { if (!isConfigAccountValid(account)) { - logger.log('@TODO'); return false; } if (accountIdsMap[account.accountId]) { - logger.log('@TODO'); + logger.debug( + i18n('config.isConfigValid.duplicateAccountIds', { + accountId: account.accountId, + }) + ); return false; } if (account.name) { if (accountNamesMap[account.name.toLowerCase()]) { - logger.log('@TODO'); + logger.debug( + i18n('config.isConfigValid.duplicateAccountNames', { + accountName: account.name, + }) + ); return false; } if (/\s+/.test(account.name)) { - logger.log('@TODO'); + logger.debug( + i18n('config.isConfigValid.invalidAccountName', { + accountName: account.name, + }) + ); return false; } accountNamesMap[account.name] = true; @@ -130,7 +141,7 @@ export function getConfigAccountById(accountId: number): HubSpotConfigAccount { ); if (!account) { - throw new Error('@TODO account not found'); + throw new Error(i18n('config.getConfigAccountById.error', { accountId })); } return account; @@ -144,7 +155,9 @@ export function getConfigAccountByName( const account = getConfigAccountByIdentifier(accounts, 'name', accountName); if (!account) { - throw new Error('@TODO account not found'); + throw new Error( + i18n('config.getConfigAccountByName.error', { accountName }) + ); } return account; @@ -154,7 +167,7 @@ export function getConfigDefaultAccount(): HubSpotConfigAccount { const { accounts, defaultAccount } = getConfig(); if (!defaultAccount) { - throw new Error('@TODO no default account'); + throw new Error(i18n('config.getConfigDefaultAccount.fieldMissingError')); } const account = getConfigAccountByIdentifier( @@ -164,7 +177,11 @@ export function getConfigDefaultAccount(): HubSpotConfigAccount { ); if (!account) { - throw new Error('@TODO no default account'); + throw new Error( + i18n('config.getConfigDefaultAccount.accountMissingError', { + defaultAccount, + }) + ); } return account; @@ -195,10 +212,9 @@ export function getConfigAccountEnvironment( return defaultAccount.env; } -// @TODO: Add logger debugs? export function addConfigAccount(accountToAdd: HubSpotConfigAccount): void { if (!isConfigAccountValid(accountToAdd)) { - throw new Error('@TODO'); + throw new Error(i18n('config.addConfigAccount.invalidAccount')); } const config = getConfig(); @@ -210,7 +226,11 @@ export function addConfigAccount(accountToAdd: HubSpotConfigAccount): void { ); if (accountInConfig) { - throw new Error('@TODO account already exists'); + throw new Error( + i18n('config.addConfigAccount.duplicateAccount', { + accountId: accountToAdd.accountId, + }) + ); } config.accounts.push(accountToAdd); @@ -222,7 +242,7 @@ export function updateConfigAccount( updatedAccount: HubSpotConfigAccount ): void { if (!isConfigAccountValid(updatedAccount)) { - throw new Error('@TODO'); + throw new Error(i18n('config.updateConfigAccount.invalidAccount')); } const config = getConfig(); @@ -233,7 +253,11 @@ export function updateConfigAccount( ); if (accountIndex < 0) { - throw new Error('@TODO account not found'); + throw new Error( + i18n('config.updateConfigAccount.accountNotFound', { + accountId: updatedAccount.accountId, + }) + ); } config.accounts[accountIndex] = updatedAccount; @@ -250,7 +274,11 @@ export function setConfigAccountAsDefault(identifier: number | string): void { ); if (!account) { - throw new Error('@TODO account not found'); + throw new Error( + i18n('config.setConfigAccountAsDefault.accountNotFound', { + accountId: identifier, + }) + ); } config.defaultAccount = account.accountId; @@ -270,7 +298,11 @@ export function renameConfigAccount( ); if (!account) { - throw new Error('@TODO account not found'); + throw new Error( + i18n('config.renameConfigAccount.accountNotFound', { + currentName, + }) + ); } const duplicateAccount = getConfigAccountByIdentifier( @@ -280,7 +312,11 @@ export function renameConfigAccount( ); if (duplicateAccount) { - throw new Error('@TODO account name already exists'); + throw new Error( + i18n('config.renameConfigAccount.duplicateAccount', { + newName, + }) + ); } account.name = newName; @@ -294,7 +330,11 @@ export function removeAccountFromConfig(accountId: number): void { const index = getConfigAccountIndexById(config.accounts, accountId); if (index < 0) { - throw new Error('@TODO account does not exist'); + throw new Error( + i18n('config.removeAccountFromConfig.accountNotFound', { + accountId, + }) + ); } config.accounts.splice(index, 1); @@ -311,7 +351,11 @@ export function updateHttpTimeout(timeout: string | number): void { typeof timeout === 'string' ? parseInt(timeout) : timeout; if (isNaN(parsedTimeout) || parsedTimeout < MIN_HTTP_TIMEOUT) { - throw new Error('@TODO timeout must be greater than min'); + throw new Error( + i18n('config.updateHttpTimeout.invalidTimeout', { + minTimeout: MIN_HTTP_TIMEOUT, + }) + ); } const config = getConfig(); @@ -336,7 +380,9 @@ export function updateDefaultCmsPublishMode( !cmsPublishMode || !Object.values(CMS_PUBLISH_MODE).includes(cmsPublishMode) ) { - throw new Error('@TODO invalid CMS publihs mode'); + throw new Error( + i18n('config.updateDefaultCmsPublishMode.invalidCmsPublishMode') + ); } const config = getConfig(); diff --git a/config/utils.ts b/config/utils.ts index 2c8a8340..a38ffe78 100644 --- a/config/utils.ts +++ b/config/utils.ts @@ -29,6 +29,7 @@ import { import { getValidEnv } from '../lib/environment'; import { getCwd } from '../lib/path'; import { CMS_PUBLISH_MODE } from '../constants/files'; +import { i18n } from '../utils/lang'; export function getGlobalConfigFilePath(): string { return path.join( @@ -59,7 +60,11 @@ export function getConfigPathEnvironmentVariables(): { process.env[ENVIRONMENT_VARIABLES.USE_ENVIRONMENT_CONFIG] === 'true'; if (configFilePathFromEnvironment && useEnvironmentConfig) { - throw new Error('@TODO'); + throw new Error( + i18n( + 'config.utils.getConfigPathEnvironmentVariables.invalidEnvironmentVariables' + ) + ); } return { @@ -74,7 +79,6 @@ export function readConfigFile(configPath: string): string { try { source = fs.readFileSync(configPath).toString(); } catch (err) { - logger.debug('@TODO Error reading'); throw new FileSystemError( { cause: err }, { @@ -169,7 +173,6 @@ export function writeConfigFile( try { fs.ensureFileSync(configPath); fs.writeFileSync(configPath, source); - logger.debug('@TODO'); } catch (err) { throw new FileSystemError( { cause: err }, @@ -240,14 +243,13 @@ export function parseConfig(configSource: string): HubSpotConfig { parsedYaml = yaml.load(configSource) as HubSpotConfig & DeprecatedHubSpotConfigFields; } catch (err) { - throw new Error('@TODO Error parsing', { cause: err }); + throw new Error(i18n('config.utils.parseConfig.error'), { cause: err }); } return normalizeParsedConfig(parsedYaml); } export function buildConfigFromEnvironment(): HubSpotConfig { - // @TODO: handle account type? 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]; @@ -268,7 +270,9 @@ export function buildConfigFromEnvironment(): HubSpotConfig { process.env[ENVIRONMENT_VARIABLES.DEFAULT_CMS_PUBLISH_MODE]; if (!accountIdVar) { - throw new Error('@TODO'); + throw new Error( + i18n('config.utils.buildConfigFromEnvironment.missingAccountId') + ); } const accountId = parseInt(accountIdVar); @@ -324,7 +328,9 @@ export function buildConfigFromEnvironment(): HubSpotConfig { name: accountIdVar, }; } else { - throw new Error('@TODO'); + throw new Error( + i18n('config.utils.buildConfigFromEnvironment.invalidAuthType') + ); } return { @@ -380,28 +386,62 @@ export function isConfigAccountValid( account: Partial ): boolean { if (!account || typeof account !== 'object') { + logger.debug(i18n('config.utils.isConfigAccountValid.missingAccount')); return false; } - if (!account.authType) { + if (!account.accountId) { + logger.debug(i18n('config.utils.isConfigAccountValid.missingAccountId')); return false; } - if (!account.accountId) { + 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) { - return 'personalAccessKey' in account && Boolean(account.personalAccessKey); + 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) { - return 'auth' in account && Boolean(account.auth); + 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) { - return 'apiKey' in account && Boolean(account.apiKey); + valid = 'apiKey' in account && Boolean(account.apiKey); + + if (!valid) { + logger.debug( + i18n('config.utils.isConfigAccountValid.missingApiKey', { + accountId: account.accountId, + }) + ); + } } - return false; + return valid; } diff --git a/http/index.ts b/http/index.ts index ca9318b0..fa52d2f0 100644 --- a/http/index.ts +++ b/http/index.ts @@ -111,7 +111,12 @@ async function withAuth( }; } - throw new Error('@TODO: invalid aut type'); + throw new Error( + i18n(`${i18nKey}.errors.invalidAuthType`, { + accountId, + authType, + }) + ); } async function getRequest( diff --git a/lang/en.json b/lang/en.json index edb27408..61ec7b59 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,80 +233,68 @@ } }, "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." - } - }, - "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" }, - "configUtils": { - "unknownType": "Unknown auth type {{ type }}" + "addConfigAccount": { + "invalidAccount": "Error adding config account: account is invalid", + "duplicateAccount": "Error adding config account: account with id {{ accountId }} already exists 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" + "updateConfigAccount": { + "invalidAccount": "Error updating config account: account is invalid", + "accountNotFound": "Error updating config account: account with id {{ accountId }} not found in config" + }, + "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_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." } } }, @@ -357,7 +346,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/personalAccessKey.ts b/lib/personalAccessKey.ts index cb16919b..f23f6bbb 100644 --- a/lib/personalAccessKey.ts +++ b/lib/personalAccessKey.ts @@ -107,7 +107,11 @@ async function getNewAccessTokenByAccountId( throw new Error(i18n(`${i18nKey}.errors.accountNotFound`, { accountId })); } if (account.authType !== PERSONAL_ACCESS_KEY_AUTH_METHOD.value) { - throw new Error('@TODO'); + throw new Error( + i18n(`${i18nKey}.errors.invalidAuthType`, { + accountId, + }) + ); } const accessTokenResponse = await getNewAccessToken(account); @@ -123,7 +127,11 @@ export async function accessTokenForPersonalAccessKey( throw new Error(i18n(`${i18nKey}.errors.accountNotFound`, { accountId })); } if (account.authType !== PERSONAL_ACCESS_KEY_AUTH_METHOD.value) { - throw new Error('@TODO'); + throw new Error( + i18n(`${i18nKey}.errors.invalidAuthType`, { + accountId, + }) + ); } const { auth } = account; diff --git a/types/Accounts.ts b/types/Accounts.ts index e05e54cc..5f3f19ba 100644 --- a/types/Accounts.ts +++ b/types/Accounts.ts @@ -12,7 +12,7 @@ export type AuthType = 'personalaccesskey' | 'apikey' | 'oauth2'; interface BaseHubSpotConfigAccount { name: string; accountId: number; - accountType?: AccountType; // @TODO: make required? + accountType?: AccountType; defaultCmsPublishMode?: CmsPublishMode; env: Environment; authType: AuthType; From 63bc4a2ad6e288618ded135e9d90b89e452fe173 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Wed, 12 Feb 2025 13:47:29 -0500 Subject: [PATCH 24/70] Handle named default accounts --- config/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/config/index.ts b/config/index.ts index fa787633..f9f2e9a8 100644 --- a/config/index.ts +++ b/config/index.ts @@ -170,9 +170,8 @@ export function getConfigDefaultAccount(): HubSpotConfigAccount { throw new Error(i18n('config.getConfigDefaultAccount.fieldMissingError')); } - const account = getConfigAccountByIdentifier( + const account = getConfigAccountByInferredIdentifier( accounts, - 'accountId', defaultAccount ); From 2533194e2407a5151b13d0c27ca42dd8b7180ba2 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Wed, 12 Feb 2025 16:47:14 -0500 Subject: [PATCH 25/70] return default account if exists --- config/CLIConfiguration.ts | 693 ------------------------------------- config/index.ts | 16 +- 2 files changed, 12 insertions(+), 697 deletions(-) delete mode 100644 config/CLIConfiguration.ts 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/index.ts b/config/index.ts index 9bade5c4..a3de571a 100644 --- a/config/index.ts +++ b/config/index.ts @@ -22,6 +22,7 @@ import { 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()); @@ -164,23 +165,30 @@ export function getConfigAccountByName( return account; } -// @TODO: handle account override export function getConfigDefaultAccount(): HubSpotConfigAccount { const { accounts, defaultAccount } = getConfig(); - if (!defaultAccount) { + let defaultAccountToUse = defaultAccount; + + if (globalConfigFileExists()) { + const defaultAccountOverrideAccountId = + getDefaultAccountOverrideAccountId(); + defaultAccountToUse = defaultAccountOverrideAccountId || defaultAccount; + } + + if (!defaultAccountToUse) { throw new Error(i18n('config.getConfigDefaultAccount.fieldMissingError')); } const account = getConfigAccountByInferredIdentifier( accounts, - defaultAccount + defaultAccountToUse ); if (!account) { throw new Error( i18n('config.getConfigDefaultAccount.accountMissingError', { - defaultAccount, + defaultAccountToUse, }) ); } From a236fa871084f93daf7ee343fabeddc034ec9431 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Wed, 12 Feb 2025 16:51:27 -0500 Subject: [PATCH 26/70] Fix tests --- config/__tests__/config.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/config/__tests__/config.test.ts b/config/__tests__/config.test.ts index 7edfe81a..0fa21adc 100644 --- a/config/__tests__/config.test.ts +++ b/config/__tests__/config.test.ts @@ -39,7 +39,7 @@ import { } from '../../constants/auth'; import { getGlobalConfigFilePath, - getLocalConfigFileDefaultPath, + getLocalConfigDefaultFilePath, formatConfigForWrite, } from '../utils'; import { CONFIG_FLAGS, ENVIRONMENT_VARIABLES } from '../../constants/config'; @@ -123,7 +123,7 @@ describe('config/index', () => { describe('localConfigFileExists()', () => { it('returns true when local config exists', () => { - mockFindup.mockReturnValueOnce(getLocalConfigFileDefaultPath()); + mockFindup.mockReturnValueOnce(getLocalConfigDefaultFilePath()); expect(localConfigFileExists()).toBe(true); }); @@ -159,8 +159,8 @@ describe('config/index', () => { it('returns local path when global does not exist', () => { mockFs.existsSync.mockReturnValueOnce(false); - mockFindup.mockReturnValueOnce(getLocalConfigFileDefaultPath()); - expect(getConfigFilePath()).toBe(getLocalConfigFileDefaultPath()); + mockFindup.mockReturnValueOnce(getLocalConfigDefaultFilePath()); + expect(getConfigFilePath()).toBe(getLocalConfigDefaultFilePath()); }); }); @@ -226,7 +226,7 @@ describe('config/index', () => { createEmptyConfigFile(false); expect(mockFs.writeFileSync).toHaveBeenCalledWith( - getLocalConfigFileDefaultPath(), + getLocalConfigDefaultFilePath(), yaml.dump({ accounts: [] }) ); }); From 1a686ed82e4447f628101c2287c9bc5816bd0904 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Wed, 12 Feb 2025 16:58:20 -0500 Subject: [PATCH 27/70] Update tests for getDefaultAccount --- config/__tests__/config.test.ts | 16 ++++++++++++++++ config/__tests__/utils.test.ts | 15 ++++++++++----- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/config/__tests__/config.test.ts b/config/__tests__/config.test.ts index 0fa21adc..a6c143ce 100644 --- a/config/__tests__/config.test.ts +++ b/config/__tests__/config.test.ts @@ -42,15 +42,22 @@ import { 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', @@ -281,6 +288,15 @@ describe('config/index', () => { expect(() => getConfigDefaultAccount()).toThrow(); }); + + it('returns the correct account when default account override is set', () => { + mockConfig({ accounts: [PAK_ACCOUNT, OAUTH_ACCOUNT] }); + mockGetDefaultAccountOverrideAccountId.mockReturnValueOnce( + OAUTH_ACCOUNT.accountId + ); + + expect(getConfigDefaultAccount()).toEqual(OAUTH_ACCOUNT); + }); }); describe('getAllConfigAccounts()', () => { diff --git a/config/__tests__/utils.test.ts b/config/__tests__/utils.test.ts index bebfe20a..e9769f37 100644 --- a/config/__tests__/utils.test.ts +++ b/config/__tests__/utils.test.ts @@ -3,7 +3,7 @@ import fs from 'fs-extra'; import { getGlobalConfigFilePath, getLocalConfigFilePath, - getLocalConfigFileDefaultPath, + getLocalConfigDefaultFilePath, getConfigPathEnvironmentVariables, readConfigFile, removeUndefinedFieldsFromConfigAccount, @@ -27,7 +27,10 @@ import { DeprecatedHubSpotConfigFields, HubSpotConfig, } from '../../types/Config'; -import { ENVIRONMENT_VARIABLES } from '../../constants/config'; +import { + ENVIRONMENT_VARIABLES, + HUBSPOT_CONFIGURATION_FOLDER, +} from '../../constants/config'; import { FileSystemError } from '../../models/FileSystemError'; import { PERSONAL_ACCESS_KEY_AUTH_METHOD, @@ -134,7 +137,9 @@ describe('config/utils', () => { it('returns the global config file path', () => { const globalConfigFilePath = getGlobalConfigFilePath(); expect(globalConfigFilePath).toBeDefined(); - expect(globalConfigFilePath).toContain('.hubspot-cli/config.yml'); + expect(globalConfigFilePath).toContain( + `${HUBSPOT_CONFIGURATION_FOLDER}/config.yml` + ); }); }); @@ -154,12 +159,12 @@ describe('config/utils', () => { }); }); - describe('getLocalConfigFileDefaultPath()', () => { + describe('getLocalConfigDefaultFilePath()', () => { it('returns the default config path in current directory', () => { const mockCwdPath = '/mock/cwd'; mockCwd.mockReturnValue(mockCwdPath); - const defaultPath = getLocalConfigFileDefaultPath(); + const defaultPath = getLocalConfigDefaultFilePath(); expect(defaultPath).toBe(`${mockCwdPath}/hubspot.config.yml`); }); }); From 48cff29dbf6f18dc5d8dbb2b1ec70aad96250db1 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Wed, 12 Feb 2025 17:11:36 -0500 Subject: [PATCH 28/70] Add tests for default account override --- .../__tests__/defaultAccountOverride.test.ts | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 config/__tests__/defaultAccountOverride.test.ts 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(); + }); + }); +}); From 1b99a2bdecc8a607a05d917fbbd11b60ca801f0d Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Wed, 12 Feb 2025 17:11:47 -0500 Subject: [PATCH 29/70] Add tests for default account override --- config/defaultAccountOverride.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/defaultAccountOverride.ts b/config/defaultAccountOverride.ts index b363482b..7ebe364d 100644 --- a/config/defaultAccountOverride.ts +++ b/config/defaultAccountOverride.ts @@ -1,5 +1,5 @@ import findup from 'findup-sync'; -import fs from 'fs'; +import fs from 'fs-extra'; import { getCwd } from '../lib/path'; import { @@ -65,7 +65,7 @@ export function getDefaultAccountOverrideAccountId(): number | null { return account.accountId; } -export function getDefaultAccountOverrideFilePath() { +export function getDefaultAccountOverrideFilePath(): string | null { return findup([DEFAULT_ACCOUNT_OVERRIDE_FILE_NAME], { cwd: getCwd(), }); From c413242ef93573030a8b3899202a6cb344cd331e Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Wed, 12 Feb 2025 17:31:22 -0500 Subject: [PATCH 30/70] Clean some things up --- config/__tests__/config.test.ts | 6 +++--- config/__tests__/utils.test.ts | 9 ++++----- config/index.ts | 16 ++++++++++------ config/utils.ts | 19 +++++++++++++------ constants/config.ts | 7 ++++++- http/index.ts | 11 ++++++++--- lang/en.json | 2 +- 7 files changed, 45 insertions(+), 25 deletions(-) diff --git a/config/__tests__/config.test.ts b/config/__tests__/config.test.ts index a6c143ce..237d193f 100644 --- a/config/__tests__/config.test.ts +++ b/config/__tests__/config.test.ts @@ -173,7 +173,8 @@ describe('config/index', () => { describe('getConfig()', () => { it('returns environment config when enabled', () => { - process.env[ENVIRONMENT_VARIABLES.USE_ENVIRONMENT_CONFIG] = 'true'; + 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'; @@ -324,8 +325,7 @@ describe('config/index', () => { describe('addConfigAccount()', () => { it('adds valid account to config', () => { mockConfig(); - // eslint-disable-next-line @typescript-eslint/no-empty-function - mockFs.writeFileSync.mockImplementationOnce(() => {}); + mockFs.writeFileSync.mockImplementationOnce(() => undefined); addConfigAccount(OAUTH_ACCOUNT); expect(mockFs.writeFileSync).toHaveBeenCalledWith( diff --git a/config/__tests__/utils.test.ts b/config/__tests__/utils.test.ts index e9769f37..65a9715e 100644 --- a/config/__tests__/utils.test.ts +++ b/config/__tests__/utils.test.ts @@ -181,7 +181,8 @@ describe('config/utils', () => { it('throws when both environment variables are set', () => { process.env[ENVIRONMENT_VARIABLES.HUBSPOT_CONFIG_PATH] = 'path'; - process.env[ENVIRONMENT_VARIABLES.USE_ENVIRONMENT_CONFIG] = 'true'; + process.env[ENVIRONMENT_VARIABLES.USE_ENVIRONMENT_HUBSPOT_CONFIG] = + 'true'; expect(() => getConfigPathEnvironmentVariables()).toThrow(); }); @@ -226,10 +227,8 @@ describe('config/utils', () => { describe('writeConfigFile()', () => { it('writes formatted config to file', () => { - // eslint-disable-next-line @typescript-eslint/no-empty-function - mockFs.ensureFileSync.mockImplementation(() => {}); - // eslint-disable-next-line @typescript-eslint/no-empty-function - mockFs.writeFileSync.mockImplementation(() => {}); + mockFs.ensureFileSync.mockImplementation(() => undefined); + mockFs.writeFileSync.mockImplementation(() => undefined); writeConfigFile(CONFIG, 'test.yml'); diff --git a/config/index.ts b/config/index.ts index a3de571a..ae82c804 100644 --- a/config/index.ts +++ b/config/index.ts @@ -1,6 +1,6 @@ import fs from 'fs-extra'; -import { MIN_HTTP_TIMEOUT } from '../constants/config'; +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'; @@ -138,7 +138,7 @@ export function getConfigAccountById(accountId: number): HubSpotConfigAccount { const account = getConfigAccountByIdentifier( accounts, - 'accountId', + ACCOUNT_IDENTIFIERS.ACCOUNT_ID, accountId ); @@ -154,7 +154,11 @@ export function getConfigAccountByName( ): HubSpotConfigAccount { const { accounts } = getConfig(); - const account = getConfigAccountByIdentifier(accounts, 'name', accountName); + const account = getConfigAccountByIdentifier( + accounts, + ACCOUNT_IDENTIFIERS.NAME, + accountName + ); if (!account) { throw new Error( @@ -230,7 +234,7 @@ export function addConfigAccount(accountToAdd: HubSpotConfigAccount): void { const accountInConfig = getConfigAccountByIdentifier( config.accounts, - 'accountId', + ACCOUNT_IDENTIFIERS.ACCOUNT_ID, accountToAdd.accountId ); @@ -302,7 +306,7 @@ export function renameConfigAccount( const account = getConfigAccountByIdentifier( config.accounts, - 'name', + ACCOUNT_IDENTIFIERS.NAME, currentName ); @@ -316,7 +320,7 @@ export function renameConfigAccount( const duplicateAccount = getConfigAccountByIdentifier( config.accounts, - 'name', + ACCOUNT_IDENTIFIERS.NAME, newName ); diff --git a/config/utils.ts b/config/utils.ts index 24dee653..d2e8000f 100644 --- a/config/utils.ts +++ b/config/utils.ts @@ -10,6 +10,7 @@ import { HUBSPOT_ACCOUNT_TYPES, DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME, ENVIRONMENT_VARIABLES, + ACCOUNT_IDENTIFIERS, } from '../constants/config'; import { PERSONAL_ACCESS_KEY_AUTH_METHOD, @@ -30,7 +31,7 @@ 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 path.join( os.homedir(), @@ -57,7 +58,8 @@ export function getConfigPathEnvironmentVariables(): { const configFilePathFromEnvironment = process.env[ENVIRONMENT_VARIABLES.HUBSPOT_CONFIG_PATH]; const useEnvironmentConfig = - process.env[ENVIRONMENT_VARIABLES.USE_ENVIRONMENT_CONFIG] === 'true'; + process.env[ENVIRONMENT_VARIABLES.USE_ENVIRONMENT_HUBSPOT_CONFIG] === + 'true'; if (configFilePathFromEnvironment && useEnvironmentConfig) { throw new Error( @@ -345,7 +347,10 @@ export function buildConfigFromEnvironment(): HubSpotConfig { export function getAccountIdentifierAndType( accountIdentifier: string | number -): { identifier: string | number; identifierType: 'name' | 'accountId' } { +): { + identifier: string | number; + identifierType: ValueOf; +} { const identifierAsNumber = typeof accountIdentifier === 'number' ? accountIdentifier @@ -354,13 +359,15 @@ export function getAccountIdentifierAndType( return { identifier: isId ? identifierAsNumber : accountIdentifier, - identifierType: isId ? 'accountId' : 'name', + identifierType: isId + ? ACCOUNT_IDENTIFIERS.ACCOUNT_ID + : ACCOUNT_IDENTIFIERS.NAME, }; } export function getConfigAccountByIdentifier( accounts: Array, - identifierFieldName: 'name' | 'accountId', + identifierFieldName: ValueOf, identifier: string | number ): HubSpotConfigAccount | undefined { return accounts.find(account => account[identifierFieldName] === identifier); @@ -377,7 +384,7 @@ export function getConfigAccountByInferredIdentifier( export function getConfigAccountIndexById( accounts: Array, - id: string | number + id: number ): number { return accounts.findIndex(account => account.accountId === id); } diff --git a/constants/config.ts b/constants/config.ts index 9d6c4c2e..977d8224 100644 --- a/constants/config.ts +++ b/constants/config.ts @@ -46,6 +46,11 @@ export const ENVIRONMENT_VARIABLES = { HTTP_USE_LOCALHOST: 'HTTP_USE_LOCALHOST', ALLOW_USAGE_TRACKING: 'ALLOW_USAGE_TRACKING', DEFAULT_CMS_PUBLISH_MODE: 'DEFUALT_CMS_PUBLISH_MODE', - USE_ENVIRONMENT_CONFIG: 'USE_ENVIRONMENT_CONFIG', + 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/http/index.ts b/http/index.ts index fa52d2f0..b28a714e 100644 --- a/http/index.ts +++ b/http/index.ts @@ -13,6 +13,11 @@ 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'; @@ -91,15 +96,15 @@ async function withAuth( getAxiosConfig({ env, ...options }) ); - if (authType === 'personalaccesskey') { + if (authType === PERSONAL_ACCESS_KEY_AUTH_METHOD.value) { return withPersonalAccessKey(accountId, axiosConfig); } - if (authType === 'oauth2') { + if (authType === OAUTH_AUTH_METHOD.value) { return withOauth(account, axiosConfig); } - if (authType === 'apikey') { + if (authType === API_KEY_AUTH_METHOD.value) { const { params } = axiosConfig; return { diff --git a/lang/en.json b/lang/en.json index 7077196a..f8a841af 100644 --- a/lang/en.json +++ b/lang/en.json @@ -287,7 +287,7 @@ "missingPersonalAccessKey": "Invalid config: account {{ accountId }} has authType of personalAccessKey but is missing the personalAccessKey field" }, "getConfigPathEnvironmentVariables": { - "invalidEnvironmentVariables": "Error loading config: USE_ENVIRONMENT_CONFIG and HUBSPOT_CONFIG_PATH cannot both be set simultaneously" + "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." From 6b5dd104686a18dc4eda2aaf5a2bfd4b01cabb2c Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Thu, 13 Feb 2025 15:19:07 -0500 Subject: [PATCH 31/70] make sandboxAccountType deprecated --- config/utils.ts | 10 ++++------ types/Accounts.ts | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/config/utils.ts b/config/utils.ts index d2e8000f..ae490407 100644 --- a/config/utils.ts +++ b/config/utils.ts @@ -207,6 +207,10 @@ export function normalizeParsedConfig( account.accountId = account.portalId; delete account.portalId; } + if (!account.accountType) { + account.accountType = getAccountType(account.sandboxAccountType); + delete account.sandboxAccountType; + } return account; }); delete parsedConfig.portals; @@ -229,12 +233,6 @@ export function normalizeParsedConfig( delete parsedConfig.defaultMode; } - parsedConfig.accounts.forEach(account => { - if (!account.accountType) { - account.accountType = getAccountType(account.sandboxAccountType); - } - }); - return parsedConfig; } diff --git a/types/Accounts.ts b/types/Accounts.ts index f0f3faac..9cf16c78 100644 --- a/types/Accounts.ts +++ b/types/Accounts.ts @@ -16,12 +16,12 @@ interface BaseHubSpotConfigAccount { defaultCmsPublishMode?: CmsPublishMode; env: Environment; authType: AuthType; - sandboxAccountType?: string; parentAccountId?: number; } export type DeprecatedHubSpotConfigAccountFields = { portalId?: number; + sandboxAccountType?: string; }; export type AccountType = ValueOf; From 46f0b308163ec821747211c72ec454cdeae0ea42 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Fri, 14 Feb 2025 14:35:41 -0500 Subject: [PATCH 32/70] Fix casing bug --- config/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/utils.ts b/config/utils.ts index ae490407..2f24f1b2 100644 --- a/config/utils.ts +++ b/config/utils.ts @@ -31,7 +31,7 @@ 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'; +import { ValueOf } from '../types/Utils'; export function getGlobalConfigFilePath(): string { return path.join( os.homedir(), From fec1d995fa369ba211a7b291b55597e52732df7d Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Thu, 13 Mar 2025 14:16:25 -0400 Subject: [PATCH 33/70] add getConfigAccountIfExists --- config/index.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/config/index.ts b/config/index.ts index ae82c804..a7060790 100644 --- a/config/index.ts +++ b/config/index.ts @@ -169,6 +169,13 @@ export function getConfigAccountByName( return account; } +export function getConfigAccountIfExists( + identifier: number | string +): HubSpotConfigAccount | undefined { + const config = getConfig(); + return getConfigAccountByInferredIdentifier(config.accounts, identifier); +} + export function getConfigDefaultAccount(): HubSpotConfigAccount { const { accounts, defaultAccount } = getConfig(); From fb7d46cdd869334bbc90bc18095deb1596d00510 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Fri, 14 Mar 2025 15:31:37 -0400 Subject: [PATCH 34/70] Update a lot of lib files --- lib/personalAccessKey.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/personalAccessKey.ts b/lib/personalAccessKey.ts index 2c83e2cd..45ef14cd 100644 --- a/lib/personalAccessKey.ts +++ b/lib/personalAccessKey.ts @@ -180,7 +180,7 @@ export async function updateConfigWithAccessToken( env?: Environment, name?: string, makeDefault = false -): Promise { +): Promise { const { portalId, accessToken, expiresAt, accountType } = token; const account = name ? getConfigAccountByName(name) From 8e68f4395693a3ae37932a230612ba20599bd35d Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Fri, 21 Mar 2025 12:38:53 -0400 Subject: [PATCH 35/70] v0.2.0-experimental.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a886abe2..8271dae4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hubspot/local-dev-lib", - "version": "3.3.1", + "version": "0.2.0-experimental.0", "description": "Provides library functionality for HubSpot local development tooling, including the HubSpot CLI", "main": "lib/index.js", "repository": { From 32433cd0b9df2c24ae285777ed2afe08b8c96ed0 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Fri, 21 Mar 2025 13:17:32 -0400 Subject: [PATCH 36/70] add http use localhost config flag --- constants/config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/constants/config.ts b/constants/config.ts index 977d8224..9bd27fe8 100644 --- a/constants/config.ts +++ b/constants/config.ts @@ -31,6 +31,7 @@ export const HUBSPOT_ACCOUNT_TYPE_STRINGS = { export const CONFIG_FLAGS = { USE_CUSTOM_OBJECT_HUBFILE: 'useCustomObjectHubfile', + HTTP_USE_LOCALHOST: 'httpUseLocalhost', } as const; export const ENVIRONMENT_VARIABLES = { From 61e2a1e8b49733102ac48c7de049315c0875e38e Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Fri, 21 Mar 2025 13:22:12 -0400 Subject: [PATCH 37/70] add getConfigDefaultAccountIfExists --- config/index.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/config/index.ts b/config/index.ts index a7060790..c7aedb2a 100644 --- a/config/index.ts +++ b/config/index.ts @@ -207,6 +207,31 @@ export function getConfigDefaultAccount(): HubSpotConfigAccount { return account; } +export function getConfigDefaultAccountIfExists(): + | HubSpotConfigAccount + | undefined { + const { accounts, defaultAccount } = getConfig(); + + let defaultAccountToUse = defaultAccount; + + if (globalConfigFileExists()) { + const defaultAccountOverrideAccountId = + getDefaultAccountOverrideAccountId(); + defaultAccountToUse = defaultAccountOverrideAccountId || defaultAccount; + } + + if (!defaultAccountToUse) { + return; + } + + const account = getConfigAccountByInferredIdentifier( + accounts, + defaultAccountToUse + ); + + return account; +} + export function getAllConfigAccounts(): HubSpotConfigAccount[] { const { accounts } = getConfig(); From 7eb31fc1bf9afaa1b959c0fd2befdd74d94f1a4f Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Fri, 4 Apr 2025 15:54:09 -0400 Subject: [PATCH 38/70] Updateconfig migrate for revamped config --- config/migrate.ts | 249 ++++++++++++++-------------------------------- lang/en.json | 3 +- 2 files changed, 76 insertions(+), 176 deletions(-) diff --git a/config/migrate.ts b/config/migrate.ts index 13fbec89..af68860f 100644 --- a/config/migrate.ts +++ b/config/migrate.ts @@ -1,218 +1,119 @@ -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(''); +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 { + 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] !== 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/lang/en.json b/lang/en.json index e4ec3893..c4b8a026 100644 --- a/lang/en.json +++ b/lang/en.json @@ -305,8 +305,7 @@ }, "migrate": { "errors": { - "writeConfig": "Unable to write global configuration file at {{ configPath }}.", - "noDeprecatedConfig": "No deprecated configuration file found. Skipping migration to global config." + "writeConfig": "Unable to write global configuration file at {{ configPath }}." } } }, From d8eab5fdbc4442321622f7d7b211b0880449de74 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Fri, 4 Apr 2025 15:55:35 -0400 Subject: [PATCH 39/70] remove unused copy --- lang/en.json | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lang/en.json b/lang/en.json index c4b8a026..f8a841af 100644 --- a/lang/en.json +++ b/lang/en.json @@ -302,11 +302,6 @@ "errorHeader": "Error in {{ hsAccountFile }}", "readFileError": "Error reading account override file." } - }, - "migrate": { - "errors": { - "writeConfig": "Unable to write global configuration file at {{ configPath }}." - } } }, "models": { From bcacccd60a17aa1ead3d332d2b86af44f3d92800 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Fri, 4 Apr 2025 16:02:13 -0400 Subject: [PATCH 40/70] export getConfigAtPath --- config/migrate.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/migrate.ts b/config/migrate.ts index af68860f..a7b9ee6e 100644 --- a/config/migrate.ts +++ b/config/migrate.ts @@ -18,7 +18,7 @@ import { } from './utils'; import { ValueOf } from '../types/Utils'; -function getConfigAtPath(path: string): HubSpotConfig { +export function getConfigAtPath(path: string): HubSpotConfig { const configFileSource = readConfigFile(path); return parseConfig(configFileSource); From bbef36275cfae49b19f541c399ab0fdf07dff5c8 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Thu, 10 Apr 2025 15:45:31 -0400 Subject: [PATCH 41/70] Updateconfig merging logic --- config/migrate.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/config/migrate.ts b/config/migrate.ts index a7b9ee6e..27fbd202 100644 --- a/config/migrate.ts +++ b/config/migrate.ts @@ -55,6 +55,13 @@ export function mergeConfigProperties( toConfig.allowUsageTracking = fromConfig.allowUsageTracking; toConfig.defaultAccount = fromConfig.defaultAccount; } else { + toConfig.defaultCmsPublishMode ||= fromConfig.defaultCmsPublishMode; + toConfig.httpTimeout ||= fromConfig.httpTimeout; + toConfig.env ||= fromConfig.env; + toConfig.httpUseLocalhost ||= fromConfig.httpUseLocalhost; + toConfig.allowUsageTracking ||= fromConfig.allowUsageTracking; + toConfig.defaultAccount ||= fromConfig.defaultAccount; + const propertiesToCheck = [ DEFAULT_CMS_PUBLISH_MODE, HTTP_TIMEOUT, @@ -65,7 +72,7 @@ export function mergeConfigProperties( ] as const; propertiesToCheck.forEach(prop => { - if (toConfig[prop] !== fromConfig[prop]) { + if (toConfig[prop] && toConfig[prop] !== fromConfig[prop]) { conflicts.push({ property: prop, oldValue: fromConfig[prop], From 1b6e75b9d20ccb97337fd54e41f4d5ea81907206 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Thu, 10 Apr 2025 17:02:55 -0400 Subject: [PATCH 42/70] Add tests for migrations --- config/__tests__/migrate.test.ts | 392 +++++++++++++++++++++++++++++++ config/migrate.ts | 12 +- 2 files changed, 401 insertions(+), 3 deletions(-) create mode 100644 config/__tests__/migrate.test.ts 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/migrate.ts b/config/migrate.ts index 27fbd202..25d81338 100644 --- a/config/migrate.ts +++ b/config/migrate.ts @@ -58,8 +58,14 @@ export function mergeConfigProperties( toConfig.defaultCmsPublishMode ||= fromConfig.defaultCmsPublishMode; toConfig.httpTimeout ||= fromConfig.httpTimeout; toConfig.env ||= fromConfig.env; - toConfig.httpUseLocalhost ||= fromConfig.httpUseLocalhost; - toConfig.allowUsageTracking ||= fromConfig.allowUsageTracking; + toConfig.httpUseLocalhost = + toConfig.httpUseLocalhost === undefined + ? fromConfig.httpUseLocalhost + : toConfig.httpUseLocalhost; + toConfig.allowUsageTracking = + toConfig.allowUsageTracking === undefined + ? fromConfig.allowUsageTracking + : toConfig.allowUsageTracking; toConfig.defaultAccount ||= fromConfig.defaultAccount; const propertiesToCheck = [ @@ -72,7 +78,7 @@ export function mergeConfigProperties( ] as const; propertiesToCheck.forEach(prop => { - if (toConfig[prop] && toConfig[prop] !== fromConfig[prop]) { + if (toConfig[prop] !== undefined && toConfig[prop] !== fromConfig[prop]) { conflicts.push({ property: prop, oldValue: fromConfig[prop], From b45cc910d3f66c25a0aa3a8c50d386d48eb7a6b8 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Tue, 28 Oct 2025 13:49:26 -0400 Subject: [PATCH 43/70] Export defaultAccountOverride --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 678dcb8d..bce73832 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "./errors/*": "./errors/*.js", "./http": "./http/index.js", "./http/*": "./http/*.js", - "./config/getAccountIdentifier": "./config/getAccountIdentifier.js", + "./config/defaultAccountOverride": "./config/defaultAccountOverride.js", "./config/migrate": "./config/migrate.js", "./config/state": "./config/state.js", "./config": "./config/index.js", From cd8cfbe84b6f311b12b4e73f383172cc92bb1309 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Thu, 30 Oct 2025 17:04:47 -0400 Subject: [PATCH 44/70] Fix types --- config/index.ts | 5 +++-- config/migrate.ts | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/config/index.ts b/config/index.ts index 954d235c..0d2f206c 100644 --- a/config/index.ts +++ b/config/index.ts @@ -23,6 +23,7 @@ import { CMS_PUBLISH_MODE } from '../constants/files'; import { Environment } from '../types/Config'; import { i18n } from '../utils/lang'; import { getDefaultAccountOverrideAccountId } from './defaultAccountOverride'; +import { getValidEnv } from '../lib/environment'; export function localConfigFileExists(): boolean { return Boolean(getLocalConfigFilePath()); @@ -250,11 +251,11 @@ export function getConfigAccountEnvironment( ); if (account) { - return account.env; + return getValidEnv(account.env); } } const defaultAccount = getConfigDefaultAccount(); - return defaultAccount.env; + return getValidEnv(defaultAccount.env); } export function addConfigAccount(accountToAdd: HubSpotConfigAccount): void { diff --git a/config/migrate.ts b/config/migrate.ts index beb053e0..16d099bf 100644 --- a/config/migrate.ts +++ b/config/migrate.ts @@ -36,7 +36,7 @@ export function migrateConfigAtPath(path: string): void { export type ConflictProperty = { property: keyof HubSpotConfig; oldValue: ValueOf; - newValue: ValueOf; + newValue: ValueOf>; }; export function mergeConfigProperties( @@ -96,7 +96,7 @@ export function mergeConfigProperties( conflicts.push({ property: prop, oldValue: fromConfig[prop], - newValue: toConfig[prop], + newValue: toConfig[prop]!, }); } }); From 59d5a52cd46fabd99f7b468e47c529c1ebc6da93 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Mon, 3 Nov 2025 16:07:40 -0500 Subject: [PATCH 45/70] add HubSpotConfigError --- config/index.ts | 161 ++++++++++++++++++++++++++--------- config/migrate.ts | 2 +- config/utils.ts | 33 +++++-- constants/config.ts | 15 ++++ errors/index.ts | 5 ++ lang/en.json | 51 ++++++----- models/HubSpotConfigError.ts | 45 ++++++++++ types/Config.ts | 10 ++- 8 files changed, 255 insertions(+), 67 deletions(-) create mode 100644 models/HubSpotConfigError.ts diff --git a/config/index.ts b/config/index.ts index 0d2f206c..58bcfb49 100644 --- a/config/index.ts +++ b/config/index.ts @@ -1,6 +1,10 @@ import fs from 'fs-extra'; -import { ACCOUNT_IDENTIFIERS, MIN_HTTP_TIMEOUT } from '../constants/config'; +import { + ACCOUNT_IDENTIFIERS, + HUBSPOT_CONFIG_OPERATIONS, + MIN_HTTP_TIMEOUT, +} from '../constants/config'; import { HubSpotConfigAccount } from '../types/Accounts'; import { HubSpotConfig, ConfigFlag } from '../types/Config'; import { CmsPublishMode } from '../types/Files'; @@ -24,6 +28,8 @@ import { Environment } from '../types/Config'; import { i18n } from '../utils/lang'; import { getDefaultAccountOverrideAccountId } from './defaultAccountOverride'; import { getValidEnv } from '../lib/environment'; +import { HubSpotConfigError } from '../models/HubSpotConfigError'; +import { HUBSPOT_CONFIG_ERROR_TYPES } from '../constants/config'; export function localConfigFileExists(): boolean { return Boolean(getLocalConfigFilePath()); @@ -43,7 +49,11 @@ function getConfigDefaultFilePath(): string { const localConfigFilePath = getLocalConfigFilePath(); if (!localConfigFilePath) { - throw new Error(i18n('config.getDefaultConfigFilePath.error')); + throw new HubSpotConfigError( + i18n('config.getDefaultConfigFilePath.error'), + HUBSPOT_CONFIG_ERROR_TYPES.CONFIG_NOT_FOUND, + HUBSPOT_CONFIG_OPERATIONS.READ + ); } return localConfigFilePath; @@ -56,18 +66,30 @@ export function getConfigFilePath(): string { } export function getConfig(): HubSpotConfig { - const { useEnvironmentConfig } = getConfigPathEnvironmentVariables(); + let pathToRead: string | undefined; + try { + const { useEnvironmentConfig } = getConfigPathEnvironmentVariables(); - if (useEnvironmentConfig) { - return buildConfigFromEnvironment(); - } + if (useEnvironmentConfig) { + return buildConfigFromEnvironment(); + } - const pathToRead = getConfigFilePath(); + pathToRead = getConfigFilePath(); - logger.debug(i18n('config.getConfig', { path: pathToRead })); - const configFileSource = readConfigFile(pathToRead); + logger.debug(i18n('config.getConfig.reading', { path: pathToRead })); + const configFileSource = readConfigFile(pathToRead); - return parseConfig(configFileSource); + return parseConfig(configFileSource, pathToRead); + } catch (err) { + throw new HubSpotConfigError( + pathToRead + ? i18n('config.getConfig.errorWithPath', { path: pathToRead }) + : i18n('config.getConfig.error'), + HUBSPOT_CONFIG_ERROR_TYPES.CONFIG_NOT_FOUND, + HUBSPOT_CONFIG_OPERATIONS.READ, + { cause: err } + ); + } } export function isConfigValid(): boolean { @@ -144,7 +166,11 @@ export function getConfigAccountById(accountId: number): HubSpotConfigAccount { ); if (!account) { - throw new Error(i18n('config.getConfigAccountById.error', { accountId })); + throw new HubSpotConfigError( + i18n('config.getConfigAccountById.error', { accountId }), + HUBSPOT_CONFIG_ERROR_TYPES.ACCOUNT_NOT_FOUND, + HUBSPOT_CONFIG_OPERATIONS.READ + ); } return account; @@ -162,8 +188,10 @@ export function getConfigAccountByName( ); if (!account) { - throw new Error( - i18n('config.getConfigAccountByName.error', { accountName }) + throw new HubSpotConfigError( + i18n('config.getConfigAccountByName.error', { accountName }), + HUBSPOT_CONFIG_ERROR_TYPES.ACCOUNT_NOT_FOUND, + HUBSPOT_CONFIG_OPERATIONS.READ ); } @@ -189,7 +217,11 @@ export function getConfigDefaultAccount(): HubSpotConfigAccount { } if (!defaultAccountToUse) { - throw new Error(i18n('config.getConfigDefaultAccount.fieldMissingError')); + throw new HubSpotConfigError( + i18n('config.getConfigDefaultAccount.fieldMissingError'), + HUBSPOT_CONFIG_ERROR_TYPES.NO_DEFAULT_ACCOUNT, + HUBSPOT_CONFIG_OPERATIONS.READ + ); } const account = getConfigAccountByInferredIdentifier( @@ -198,10 +230,12 @@ export function getConfigDefaultAccount(): HubSpotConfigAccount { ); if (!account) { - throw new Error( + throw new HubSpotConfigError( i18n('config.getConfigDefaultAccount.accountMissingError', { defaultAccountToUse, - }) + }), + HUBSPOT_CONFIG_ERROR_TYPES.ACCOUNT_NOT_FOUND, + HUBSPOT_CONFIG_OPERATIONS.READ ); } @@ -260,7 +294,11 @@ export function getConfigAccountEnvironment( export function addConfigAccount(accountToAdd: HubSpotConfigAccount): void { if (!isConfigAccountValid(accountToAdd)) { - throw new Error(i18n('config.addConfigAccount.invalidAccount')); + throw new HubSpotConfigError( + i18n('config.addConfigAccount.invalidAccount'), + HUBSPOT_CONFIG_ERROR_TYPES.INVALID_ACCOUNT, + HUBSPOT_CONFIG_OPERATIONS.WRITE + ); } const config = getConfig(); @@ -272,10 +310,12 @@ export function addConfigAccount(accountToAdd: HubSpotConfigAccount): void { ); if (accountInConfig) { - throw new Error( + throw new HubSpotConfigError( i18n('config.addConfigAccount.duplicateAccount', { accountId: accountToAdd.accountId, - }) + }), + HUBSPOT_CONFIG_ERROR_TYPES.INVALID_ACCOUNT, + HUBSPOT_CONFIG_OPERATIONS.WRITE ); } @@ -288,7 +328,13 @@ export function updateConfigAccount( updatedAccount: HubSpotConfigAccount ): void { if (!isConfigAccountValid(updatedAccount)) { - throw new Error(i18n('config.updateConfigAccount.invalidAccount')); + throw new HubSpotConfigError( + i18n('config.updateConfigAccount.invalidAccount', { + name: updatedAccount.name, + }), + HUBSPOT_CONFIG_ERROR_TYPES.INVALID_ACCOUNT, + HUBSPOT_CONFIG_OPERATIONS.WRITE + ); } const config = getConfig(); @@ -299,10 +345,12 @@ export function updateConfigAccount( ); if (accountIndex < 0) { - throw new Error( + throw new HubSpotConfigError( i18n('config.updateConfigAccount.accountNotFound', { accountId: updatedAccount.accountId, - }) + }), + HUBSPOT_CONFIG_ERROR_TYPES.ACCOUNT_NOT_FOUND, + HUBSPOT_CONFIG_OPERATIONS.WRITE ); } @@ -320,10 +368,12 @@ export function setConfigAccountAsDefault(identifier: number | string): void { ); if (!account) { - throw new Error( + throw new HubSpotConfigError( i18n('config.setConfigAccountAsDefault.accountNotFound', { accountId: identifier, - }) + }), + HUBSPOT_CONFIG_ERROR_TYPES.ACCOUNT_NOT_FOUND, + HUBSPOT_CONFIG_OPERATIONS.WRITE ); } @@ -344,10 +394,12 @@ export function renameConfigAccount( ); if (!account) { - throw new Error( + throw new HubSpotConfigError( i18n('config.renameConfigAccount.accountNotFound', { currentName, - }) + }), + HUBSPOT_CONFIG_ERROR_TYPES.ACCOUNT_NOT_FOUND, + HUBSPOT_CONFIG_OPERATIONS.WRITE ); } @@ -358,10 +410,13 @@ export function renameConfigAccount( ); if (duplicateAccount) { - throw new Error( + throw new HubSpotConfigError( i18n('config.renameConfigAccount.duplicateAccount', { + currentName, newName, - }) + }), + HUBSPOT_CONFIG_ERROR_TYPES.INVALID_ACCOUNT, + HUBSPOT_CONFIG_OPERATIONS.WRITE ); } @@ -376,10 +431,12 @@ export function removeAccountFromConfig(accountId: number): void { const index = getConfigAccountIndexById(config.accounts, accountId); if (index < 0) { - throw new Error( + throw new HubSpotConfigError( i18n('config.removeAccountFromConfig.accountNotFound', { accountId, - }) + }), + HUBSPOT_CONFIG_ERROR_TYPES.ACCOUNT_NOT_FOUND, + HUBSPOT_CONFIG_OPERATIONS.WRITE ); } @@ -397,10 +454,13 @@ export function updateHttpTimeout(timeout: string | number): void { typeof timeout === 'string' ? parseInt(timeout) : timeout; if (isNaN(parsedTimeout) || parsedTimeout < MIN_HTTP_TIMEOUT) { - throw new Error( + throw new HubSpotConfigError( i18n('config.updateHttpTimeout.invalidTimeout', { minTimeout: MIN_HTTP_TIMEOUT, - }) + timeout: parsedTimeout, + }), + HUBSPOT_CONFIG_ERROR_TYPES.INVALID_FIELD, + HUBSPOT_CONFIG_OPERATIONS.WRITE ); } @@ -412,6 +472,16 @@ export function updateHttpTimeout(timeout: string | number): void { } export function updateAllowUsageTracking(isAllowed: boolean): void { + if (typeof isAllowed !== 'boolean') { + throw new HubSpotConfigError( + i18n('config.updateAllowUsageTracking.invalidInput', { + isAllowed: `${isAllowed}`, + }), + HUBSPOT_CONFIG_ERROR_TYPES.INVALID_FIELD, + HUBSPOT_CONFIG_OPERATIONS.WRITE + ); + } + const config = getConfig(); config.allowUsageTracking = isAllowed; @@ -419,20 +489,31 @@ export function updateAllowUsageTracking(isAllowed: boolean): void { writeConfigFile(config, getConfigFilePath()); } -export function updateAllowAutoUpdates(enabled: boolean): void { +export function updateAllowAutoUpdates(isEnabled: boolean): void { + if (typeof isEnabled !== 'boolean') { + throw new HubSpotConfigError( + i18n('config.updateAllowAutoUpdates.invalidInput', { + isEnabled: `${isEnabled}`, + }), + HUBSPOT_CONFIG_ERROR_TYPES.INVALID_FIELD, + HUBSPOT_CONFIG_OPERATIONS.WRITE + ); + } const config = getConfig(); - config.allowAutoUpdates = enabled; + config.allowAutoUpdates = isEnabled; writeConfigFile(config, getConfigFilePath()); } export function updateAutoOpenBrowser(isEnabled: boolean): void { if (typeof isEnabled !== 'boolean') { - throw new Error( + throw new HubSpotConfigError( i18n('config.updateAutoOpenBrowser.invalidInput', { isEnabled: `${isEnabled}`, - }) + }), + HUBSPOT_CONFIG_ERROR_TYPES.INVALID_FIELD, + HUBSPOT_CONFIG_OPERATIONS.WRITE ); } @@ -450,8 +531,12 @@ export function updateDefaultCmsPublishMode( !cmsPublishMode || !Object.values(CMS_PUBLISH_MODE).includes(cmsPublishMode) ) { - throw new Error( - i18n('config.updateDefaultCmsPublishMode.invalidCmsPublishMode') + throw new HubSpotConfigError( + i18n('config.updateDefaultCmsPublishMode.invalidCmsPublishMode', { + cmsPublishMode, + }), + HUBSPOT_CONFIG_ERROR_TYPES.INVALID_FIELD, + HUBSPOT_CONFIG_OPERATIONS.WRITE ); } diff --git a/config/migrate.ts b/config/migrate.ts index 16d099bf..42b40bbf 100644 --- a/config/migrate.ts +++ b/config/migrate.ts @@ -23,7 +23,7 @@ import { ValueOf } from '../types/Utils'; export function getConfigAtPath(path: string): HubSpotConfig { const configFileSource = readConfigFile(path); - return parseConfig(configFileSource); + return parseConfig(configFileSource, path); } export function migrateConfigAtPath(path: string): void { diff --git a/config/utils.ts b/config/utils.ts index 77db76c1..54c8d2b7 100644 --- a/config/utils.ts +++ b/config/utils.ts @@ -8,6 +8,8 @@ import { ENVIRONMENT_VARIABLES, ACCOUNT_IDENTIFIERS, GLOBAL_CONFIG_PATH, + HUBSPOT_CONFIG_ERROR_TYPES, + HUBSPOT_CONFIG_OPERATIONS, } from '../constants/config'; import { PERSONAL_ACCESS_KEY_AUTH_METHOD, @@ -29,6 +31,7 @@ import { getCwd } from '../lib/path'; import { CMS_PUBLISH_MODE } from '../constants/files'; import { i18n } from '../utils/lang'; import { ValueOf } from '../types/Utils'; +import { HubSpotConfigError } from '../models/HubSpotConfigError'; export function getGlobalConfigFilePath(): string { return GLOBAL_CONFIG_PATH; @@ -56,10 +59,12 @@ export function getConfigPathEnvironmentVariables(): { 'true'; if (configFilePathFromEnvironment && useEnvironmentConfig) { - throw new Error( + throw new HubSpotConfigError( i18n( 'config.utils.getConfigPathEnvironmentVariables.invalidEnvironmentVariables' - ) + ), + HUBSPOT_CONFIG_ERROR_TYPES.INVALID_ENVIRONMENT_VARIABLES, + HUBSPOT_CONFIG_OPERATIONS.READ ); } @@ -230,14 +235,22 @@ export function normalizeParsedConfig( return parsedConfig; } -export function parseConfig(configSource: string): HubSpotConfig { +export function parseConfig( + configSource: string, + configPath: 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 }); + throw new HubSpotConfigError( + i18n('config.utils.parseConfig.error', { configPath: configPath }), + HUBSPOT_CONFIG_ERROR_TYPES.YAML_PARSING, + HUBSPOT_CONFIG_OPERATIONS.READ, + { cause: err } + ); } return normalizeParsedConfig(parsedYaml); @@ -264,8 +277,10 @@ export function buildConfigFromEnvironment(): HubSpotConfig { process.env[ENVIRONMENT_VARIABLES.DEFAULT_CMS_PUBLISH_MODE]; if (!accountIdVar) { - throw new Error( - i18n('config.utils.buildConfigFromEnvironment.missingAccountId') + throw new HubSpotConfigError( + i18n('config.utils.buildConfigFromEnvironment.missingAccountId'), + HUBSPOT_CONFIG_ERROR_TYPES.INVALID_ENVIRONMENT_VARIABLES, + HUBSPOT_CONFIG_OPERATIONS.READ ); } @@ -322,8 +337,10 @@ export function buildConfigFromEnvironment(): HubSpotConfig { name: accountIdVar, }; } else { - throw new Error( - i18n('config.utils.buildConfigFromEnvironment.invalidAuthType') + throw new HubSpotConfigError( + i18n('config.utils.buildConfigFromEnvironment.invalidAuthType'), + HUBSPOT_CONFIG_ERROR_TYPES.INVALID_ENVIRONMENT_VARIABLES, + HUBSPOT_CONFIG_OPERATIONS.READ ); } diff --git a/constants/config.ts b/constants/config.ts index 89bb828c..af5fd09e 100644 --- a/constants/config.ts +++ b/constants/config.ts @@ -82,3 +82,18 @@ export const ACCOUNT_IDENTIFIERS = { ACCOUNT_ID: 'accountId', NAME: 'name', } as const; + +export const HUBSPOT_CONFIG_ERROR_TYPES = { + CONFIG_NOT_FOUND: 'CONFIG_NOT_FOUND', + ACCOUNT_NOT_FOUND: 'ACCOUNT_NOT_FOUND', + NO_DEFAULT_ACCOUNT: 'NO_DEFAULT_ACCOUNT', + INVALID_ENVIRONMENT_VARIABLES: 'ENVIRONMENT_VARIABLES', + YAML_PARSING: 'YAML_PARSING', + INVALID_ACCOUNT: 'INVALID_ACCOUNT', + INVALID_FIELD: 'INVALID_FIELD', +} as const; + +export const HUBSPOT_CONFIG_OPERATIONS = { + READ: 'READ', + WRITE: 'WRITE', +} as const; diff --git a/errors/index.ts b/errors/index.ts index 9840fbde..77ba5612 100644 --- a/errors/index.ts +++ b/errors/index.ts @@ -1,6 +1,7 @@ import { HubSpotHttpError } from '../models/HubSpotHttpError'; import { BaseError } from '../types/Error'; import { FileSystemError } from '../models/FileSystemError'; +import { HubSpotConfigError } from '../models/HubSpotConfigError'; export function isSpecifiedError( err: unknown, @@ -97,3 +98,7 @@ export function isSystemError(err: unknown): err is BaseError { export function isFileSystemError(err: unknown): err is FileSystemError { return err instanceof FileSystemError; } + +export function isHubSpotConfigError(err: unknown): err is HubSpotConfigError { + return err instanceof HubSpotConfigError; +} diff --git a/lang/en.json b/lang/en.json index cf37ab62..f12fbd70 100644 --- a/lang/en.json +++ b/lang/en.json @@ -244,9 +244,13 @@ }, "config": { "getDefaultConfigFilePath": { - "error": "Error getting config file path: no config file found" + "error": "Attempted to get config file path, but no config file was found." + }, + "getConfig": { + "reading": "Reading config from {{ path }}", + "error": "No config file found.", + "errorWithPath": "No config file found at {{ path }}." }, - "getConfig": "Reading config from {{ path }}", "isConfigValid": { "missingAccounts": "Invalid config: no accounts found", "duplicateAccountIds": "Invalid config: multiple accounts with accountId: {{ accountId }}", @@ -254,35 +258,35 @@ "invalidAccountName": "Invalid config: account name {{ accountName }} contains spaces" }, "getConfigAccountById": { - "error": "Error getting config account: no account with id {{ accountId }} exists in config" + "error": "No account with id {{ accountId }} exists in config" }, "getConfigAccountByName": { - "error": "Error getting config account: no account with name {{ accountName }} exists in config" + "error": "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" + "fieldMissingError": "Attempted to get default account, but no default account was found in config", + "accountMissingError": "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" + "invalidAccount": "Attempting to add account, but account is invalid", + "duplicateAccount": "Attempting to add account, but account with id {{ accountId }} already exists in config" }, "updateConfigAccount": { - "invalidAccount": "Error updating config account: account is invalid", - "accountNotFound": "Error updating config account: account with id {{ accountId }} not found in config" + "invalidAccount": "Attempting to update account {{ name }}, but account is invalid", + "accountNotFound": "Attempting to update account with id {{ id }}, but that account was not found in config" }, "setConfigAccountAsDefault": { - "accountNotFound": "Error setting config default account: account with id {{ accountId }} not found in config" + "accountNotFound": "Attempted to set account with id {{ accountId }} as default, but that account was 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" + "accountNotFound": "Attempted to rename account with name {{ currentName }}, but that account was not found in config", + "duplicateAccount": "Attempted to rename account {{ currentName }} to {{ newName }}, but an account with that name already exists in config" }, "removeAccountFromConfig": { - "accountNotFound": "Error removing config account: account with id {{ accountId }} not found in config" + "accountNotFound": "Attempted to remove account with id {{ accountId }}, but that account was not found in config" }, "updateHttpTimeout": { - "invalidTimeout": "Error updating config http timeout: timeout must be greater than {{ minTimeout }}" + "invalidTimeout": "HTTP timeout must be greater than {{ minTimeout }}. Received {{ timeout }}" }, "updateDefaultCmsPublishMode": { "invalidCmsPublishMode": "Error updating config default CMS publish mode: CMS publish can only be set to 'draft' or 'publish'" @@ -297,14 +301,14 @@ "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" + "invalidEnvironmentVariables": "USE_ENVIRONMENT_HUBSPOT_CONFIG and HUBSPOT_CONFIG_PATH cannot both be set simultaneously" }, "parseConfig": { - "error": "An error occurred parsing the config file." + "error": "File could not be parsed. Confirm that your config file {{ configPath }} is valid YAML." }, "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." + "missingAccountId": "HUBSPOT_ACCOUNT_ID is required, but not currently set", + "invalidAuthType": "Auth type 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." } }, "defaultAccountOverride": { @@ -313,6 +317,12 @@ "readFileError": "Error reading account override file." } }, + "updateAllowUsageTracking": { + "invalidInput": "Unable to update allowUsageTracking. The value {{ isAllowed }} is invalid. The value must be a boolean." + }, + "updateAllowAutoUpdates": { + "invalidInput": "Unable to update allowAutoUpdates. The value {{ isEnabled }} is invalid. The value must be a boolean." + }, "updateAutoOpenBrowser": { "invalidInput": "Unable to update autoOpenBrowser. The value {{ isEnabled }} is invalid. The value must be a boolean." }, @@ -367,6 +377,9 @@ "missingRefreshToken": "The account {{ accountId }} has not been authenticated with Oauth2", "auth": "Error while retrieving new token: {{ token }}" } + }, + "HubSpotConfigError": { + "baseMessage": "An error occurred while {{ operation }} your HubSpot config {{ configType }}: {{ message }}" } }, "utils": { diff --git a/models/HubSpotConfigError.ts b/models/HubSpotConfigError.ts new file mode 100644 index 00000000..f761b5d9 --- /dev/null +++ b/models/HubSpotConfigError.ts @@ -0,0 +1,45 @@ +import { + HUBSPOT_CONFIG_ERROR_TYPES, + HUBSPOT_CONFIG_OPERATIONS, +} from '../constants/config'; +import { + HubSpotConfigErrorType, + HubSpotConfigOperation, +} from '../types/Config'; +import { i18n } from '../utils/lang'; + +const NAME = 'HubSpotConfigError'; + +function isEnvironmentError(type: HubSpotConfigErrorType): boolean { + return type === HUBSPOT_CONFIG_ERROR_TYPES.INVALID_ENVIRONMENT_VARIABLES; +} + +export class HubSpotConfigError extends Error { + public type: HubSpotConfigErrorType; + public operation: HubSpotConfigOperation; + + constructor( + message: string, + type: HubSpotConfigErrorType, + operation: HubSpotConfigOperation, + options?: ErrorOptions + ) { + const configType = isEnvironmentError(type) + ? 'environment variables' + : 'file'; + + const operationText = + operation === HUBSPOT_CONFIG_OPERATIONS.WRITE ? 'writing to' : 'reading'; + + const withBaseMessage = i18n('models.HubSpotConfigError.baseMessage', { + configType, + message, + operation: operationText, + }); + + super(withBaseMessage, options); + this.name = NAME; + this.type = type; + this.operation = operation; + } +} diff --git a/types/Config.ts b/types/Config.ts index edb8eeec..cedfd3e2 100644 --- a/types/Config.ts +++ b/types/Config.ts @@ -1,4 +1,8 @@ -import { CONFIG_FLAGS } from '../constants/config'; +import { + CONFIG_FLAGS, + HUBSPOT_CONFIG_ERROR_TYPES, + HUBSPOT_CONFIG_OPERATIONS, +} from '../constants/config'; import { ENVIRONMENTS } from '../constants/environments'; import { DeprecatedHubSpotConfigAccountFields, @@ -41,3 +45,7 @@ export type ConfigFlag = ValueOf; export type CLIState = { mcpTotalToolCalls: number; }; + +export type HubSpotConfigErrorType = ValueOf; + +export type HubSpotConfigOperation = ValueOf; From fbf468df606e2c3bc118676832405bc3bf701a8f Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Wed, 12 Nov 2025 12:33:12 -0500 Subject: [PATCH 46/70] Fix updateConfigWithAccessToken --- lib/__tests__/personalAccessKey.test.ts | 357 ++++++++++++++++++++++++ lib/personalAccessKey.ts | 24 +- 2 files changed, 372 insertions(+), 9 deletions(-) diff --git a/lib/__tests__/personalAccessKey.test.ts b/lib/__tests__/personalAccessKey.test.ts index 364e12ed..cf330cce 100644 --- a/lib/__tests__/personalAccessKey.test.ts +++ b/lib/__tests__/personalAccessKey.test.ts @@ -3,6 +3,10 @@ import { getConfig as __getConfig, getConfigAccountById as __getConfigAccountById, updateConfigAccount as __updateConfigAccount, + addConfigAccount as __addConfigAccount, + setConfigAccountAsDefault as __setConfigAccountAsDefault, + getConfigAccountIfExists as __getConfigAccountIfExists, + getConfigDefaultAccountIfExists as __getConfigDefaultAccountIfExists, } from '../../config'; import { fetchAccessToken as __fetchAccessToken } from '../../api/localDevAuth'; import { fetchSandboxHubData as __fetchSandboxHubData } from '../../api/sandboxHubs'; @@ -26,6 +30,21 @@ jest.mock('../../api/developerTestAccounts'); const updateConfigAccount = __updateConfigAccount as jest.MockedFunction< typeof __updateConfigAccount >; +const addConfigAccount = __addConfigAccount as jest.MockedFunction< + typeof __addConfigAccount +>; +const setConfigAccountAsDefault = + __setConfigAccountAsDefault as jest.MockedFunction< + typeof __setConfigAccountAsDefault + >; +const getConfigAccountIfExists = + __getConfigAccountIfExists as jest.MockedFunction< + typeof __getConfigAccountIfExists + >; +const getConfigDefaultAccountIfExists = + __getConfigDefaultAccountIfExists as jest.MockedFunction< + typeof __getConfigDefaultAccountIfExists + >; const getConfigAccountById = __getConfigAccountById as jest.MockedFunction< typeof __getConfigAccountById >; @@ -216,6 +235,21 @@ describe('lib/personalAccessKey', () => { describe('updateConfigWithPersonalAccessKey()', () => { it('updates the config with the new account', async () => { + const existingAccount = { + accountId: 123, + name: 'account-name', + authType: 'personalaccesskey' as const, + personalAccessKey: 'old-key', + env: ENVIRONMENTS.QA, + auth: { + tokenInfo: { + accessToken: 'old-token', + expiresAt: moment().add(1, 'hours').toISOString(), + }, + }, + }; + getConfigAccountIfExists.mockReturnValue(existingAccount); + const freshAccessToken = 'fresh-token'; fetchAccessToken.mockResolvedValue( mockAxiosResponse({ @@ -251,6 +285,21 @@ describe('lib/personalAccessKey', () => { }); it('updates the config with the new account for sandbox accounts', async () => { + const existingAccount = { + accountId: 123, + name: 'account-name', + authType: 'personalaccesskey' as const, + personalAccessKey: 'old-key', + env: ENVIRONMENTS.QA, + auth: { + tokenInfo: { + accessToken: 'old-token', + expiresAt: moment().add(1, 'hours').toISOString(), + }, + }, + }; + getConfigAccountIfExists.mockReturnValue(existingAccount); + fetchSandboxHubData.mockResolvedValue( mockAxiosResponse({ type: 'DEVELOPER', @@ -294,6 +343,21 @@ describe('lib/personalAccessKey', () => { }); it('updates the config with the new account for developer test accounts', async () => { + const existingAccount = { + accountId: 123, + name: 'Dev test portal', + authType: 'personalaccesskey' as const, + personalAccessKey: 'old-key', + env: ENVIRONMENTS.QA, + auth: { + tokenInfo: { + accessToken: 'old-token', + expiresAt: moment().add(1, 'hours').toISOString(), + }, + }, + }; + getConfigAccountIfExists.mockReturnValue(existingAccount); + fetchSandboxHubData.mockRejectedValue(new Error('Not a sandbox')); fetchDeveloperTestAccountData.mockResolvedValue( mockAxiosResponse({ @@ -340,5 +404,298 @@ describe('lib/personalAccessKey', () => { }) ); }); + + it('adds a new account when account does not exist', async () => { + getConfigAccountIfExists.mockReturnValue(undefined); + getConfigDefaultAccountIfExists.mockReturnValue(undefined); + + const freshAccessToken = 'fresh-token'; + fetchAccessToken.mockResolvedValue( + mockAxiosResponse({ + oauthAccessToken: freshAccessToken, + expiresAtMillis: moment().add(1, 'hours').valueOf(), + encodedOAuthRefreshToken: 'let-me-in-6', + scopeGroups: ['content'], + hubId: 123, + userId: 456, + hubName: 'test-hub', + accountType: HUBSPOT_ACCOUNT_TYPES.STANDARD, + }) + ); + + const token = await getAccessToken('pak_123', ENVIRONMENTS.QA, 123); + + await updateConfigWithAccessToken( + token, + 'pak_123', + ENVIRONMENTS.QA, + 'new-account' + ); + + expect(addConfigAccount).toHaveBeenCalledWith( + expect.objectContaining({ + accountId: 123, + accountType: HUBSPOT_ACCOUNT_TYPES.STANDARD, + personalAccessKey: 'pak_123', + name: 'new-account', + authType: 'personalaccesskey', + }) + ); + expect(updateConfigAccount).not.toHaveBeenCalled(); + }); + + it('updates existing account when account exists', async () => { + const existingAccount = { + accountId: 123, + name: 'existing-account', + authType: 'personalaccesskey' as const, + personalAccessKey: 'old-key', + env: ENVIRONMENTS.PROD, + auth: { + tokenInfo: { + accessToken: 'old-token', + expiresAt: moment().add(1, 'hours').toISOString(), + }, + }, + }; + getConfigAccountIfExists.mockReturnValue(existingAccount); + + const freshAccessToken = 'fresh-token'; + fetchAccessToken.mockResolvedValue( + mockAxiosResponse({ + oauthAccessToken: freshAccessToken, + expiresAtMillis: moment().add(1, 'hours').valueOf(), + encodedOAuthRefreshToken: 'let-me-in-7', + scopeGroups: ['content'], + hubId: 123, + userId: 456, + hubName: 'test-hub', + accountType: HUBSPOT_ACCOUNT_TYPES.STANDARD, + }) + ); + + const token = await getAccessToken('pak_123', ENVIRONMENTS.QA, 123); + + await updateConfigWithAccessToken( + token, + 'pak_123', + ENVIRONMENTS.QA, + 'existing-account' + ); + + expect(updateConfigAccount).toHaveBeenCalledWith( + expect.objectContaining({ + accountId: 123, + personalAccessKey: 'pak_123', + name: 'existing-account', + }) + ); + expect(addConfigAccount).not.toHaveBeenCalled(); + }); + + it('sets account as default when makeDefault is true', async () => { + getConfigAccountIfExists.mockReturnValue(undefined); + + const freshAccessToken = 'fresh-token'; + fetchAccessToken.mockResolvedValue( + mockAxiosResponse({ + oauthAccessToken: freshAccessToken, + expiresAtMillis: moment().add(1, 'hours').valueOf(), + encodedOAuthRefreshToken: 'let-me-in-8', + scopeGroups: ['content'], + hubId: 123, + userId: 456, + hubName: 'test-hub', + accountType: HUBSPOT_ACCOUNT_TYPES.STANDARD, + }) + ); + + const token = await getAccessToken('pak_123', ENVIRONMENTS.QA, 123); + + await updateConfigWithAccessToken( + token, + 'pak_123', + ENVIRONMENTS.QA, + 'default-account', + true + ); + + expect(setConfigAccountAsDefault).toHaveBeenCalledWith(123); + }); + + it('does not set account as default when makeDefault is false', async () => { + getConfigAccountIfExists.mockReturnValue(undefined); + + const freshAccessToken = 'fresh-token'; + fetchAccessToken.mockResolvedValue( + mockAxiosResponse({ + oauthAccessToken: freshAccessToken, + expiresAtMillis: moment().add(1, 'hours').valueOf(), + encodedOAuthRefreshToken: 'let-me-in-9', + scopeGroups: ['content'], + hubId: 123, + userId: 456, + hubName: 'test-hub', + accountType: HUBSPOT_ACCOUNT_TYPES.STANDARD, + }) + ); + + const token = await getAccessToken('pak_123', ENVIRONMENTS.QA, 123); + + await updateConfigWithAccessToken( + token, + 'pak_123', + ENVIRONMENTS.QA, + 'not-default-account', + false + ); + + expect(setConfigAccountAsDefault).not.toHaveBeenCalled(); + }); + + it('defaults environment to PROD when not provided and no existing account', async () => { + getConfigAccountIfExists.mockReturnValue(undefined); + getConfigDefaultAccountIfExists.mockReturnValue(undefined); + + const freshAccessToken = 'fresh-token'; + fetchAccessToken.mockResolvedValue( + mockAxiosResponse({ + oauthAccessToken: freshAccessToken, + expiresAtMillis: moment().add(1, 'hours').valueOf(), + encodedOAuthRefreshToken: 'let-me-in-10', + scopeGroups: ['content'], + hubId: 123, + userId: 456, + hubName: 'test-hub', + accountType: HUBSPOT_ACCOUNT_TYPES.STANDARD, + }) + ); + + const token = await getAccessToken('pak_123', undefined, 123); + + await updateConfigWithAccessToken(token, 'pak_123', undefined, 'account'); + + expect(addConfigAccount).toHaveBeenCalledWith( + expect.objectContaining({ + env: ENVIRONMENTS.PROD, + }) + ); + }); + + it('uses existing account environment when env not provided', async () => { + const existingAccount = { + accountId: 123, + name: 'existing-account', + authType: 'personalaccesskey' as const, + personalAccessKey: 'old-key', + env: ENVIRONMENTS.QA, + auth: { + tokenInfo: { + accessToken: 'old-token', + expiresAt: moment().add(1, 'hours').toISOString(), + }, + }, + }; + getConfigAccountIfExists.mockReturnValue(existingAccount); + + const freshAccessToken = 'fresh-token'; + fetchAccessToken.mockResolvedValue( + mockAxiosResponse({ + oauthAccessToken: freshAccessToken, + expiresAtMillis: moment().add(1, 'hours').valueOf(), + encodedOAuthRefreshToken: 'let-me-in-11', + scopeGroups: ['content'], + hubId: 123, + userId: 456, + hubName: 'test-hub', + accountType: HUBSPOT_ACCOUNT_TYPES.STANDARD, + }) + ); + + const token = await getAccessToken('pak_123', ENVIRONMENTS.PROD, 123); + + await updateConfigWithAccessToken( + token, + 'pak_123', + undefined, + 'existing-account' + ); + + expect(updateConfigAccount).toHaveBeenCalledWith( + expect.objectContaining({ + env: ENVIRONMENTS.QA, + }) + ); + }); + + it('uses token hubName when name not provided and no existing account', async () => { + getConfigAccountIfExists.mockReturnValue(undefined); + getConfigDefaultAccountIfExists.mockReturnValue(undefined); + + const freshAccessToken = 'fresh-token'; + fetchAccessToken.mockResolvedValue( + mockAxiosResponse({ + oauthAccessToken: freshAccessToken, + expiresAtMillis: moment().add(1, 'hours').valueOf(), + encodedOAuthRefreshToken: 'let-me-in-12', + scopeGroups: ['content'], + hubId: 123, + userId: 456, + hubName: 'hub-from-token', + accountType: HUBSPOT_ACCOUNT_TYPES.STANDARD, + }) + ); + + const token = await getAccessToken('pak_123', ENVIRONMENTS.QA, 123); + + await updateConfigWithAccessToken(token, 'pak_123', ENVIRONMENTS.QA); + + expect(addConfigAccount).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'hub-from-token', + }) + ); + }); + + it('uses existing account name when name not provided', async () => { + const existingAccount = { + accountId: 123, + name: 'existing-name', + authType: 'personalaccesskey' as const, + personalAccessKey: 'old-key', + env: ENVIRONMENTS.PROD, + auth: { + tokenInfo: { + accessToken: 'old-token', + expiresAt: moment().add(1, 'hours').toISOString(), + }, + }, + }; + getConfigDefaultAccountIfExists.mockReturnValue(existingAccount); + + const freshAccessToken = 'fresh-token'; + fetchAccessToken.mockResolvedValue( + mockAxiosResponse({ + oauthAccessToken: freshAccessToken, + expiresAtMillis: moment().add(1, 'hours').valueOf(), + encodedOAuthRefreshToken: 'let-me-in-13', + scopeGroups: ['content'], + hubId: 123, + userId: 456, + hubName: 'hub-from-token', + accountType: HUBSPOT_ACCOUNT_TYPES.STANDARD, + }) + ); + + const token = await getAccessToken('pak_123', ENVIRONMENTS.QA, 123); + + await updateConfigWithAccessToken(token, 'pak_123', ENVIRONMENTS.QA); + + expect(updateConfigAccount).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'existing-name', + }) + ); + }); }); }); diff --git a/lib/personalAccessKey.ts b/lib/personalAccessKey.ts index 45ef14cd..7addff7b 100644 --- a/lib/personalAccessKey.ts +++ b/lib/personalAccessKey.ts @@ -13,10 +13,11 @@ import { import { Environment } from '../types/Config'; import { getConfigAccountById, - getConfigAccountByName, + getConfigAccountIfExists, + getConfigDefaultAccountIfExists, updateConfigAccount, + addConfigAccount, setConfigAccountAsDefault, - getConfigDefaultAccount, } from '../config'; import { HUBSPOT_ACCOUNT_TYPES } from '../constants/config'; import { fetchDeveloperTestAccountData } from '../api/developerTestAccounts'; @@ -183,9 +184,9 @@ export async function updateConfigWithAccessToken( ): Promise { const { portalId, accessToken, expiresAt, accountType } = token; const account = name - ? getConfigAccountByName(name) - : getConfigDefaultAccount(); - const accountEnv = env || account.env; + ? getConfigAccountIfExists(name) + : getConfigDefaultAccountIfExists(); + const accountEnv = env || account?.env || ENVIRONMENTS.PROD; let parentAccountId; try { @@ -230,17 +231,22 @@ export async function updateConfigWithAccessToken( accountId: portalId, accountType, personalAccessKey, - name: name || account.name, + name: name || account?.name || token.hubName, authType: PERSONAL_ACCESS_KEY_AUTH_METHOD.value, auth: { tokenInfo: { accessToken, expiresAt } }, parentAccountId, env: accountEnv, }; - updateConfigAccount(updatedAccount); + // Add new account if it doesn't exist, otherwise update existing account + if (account) { + updateConfigAccount(updatedAccount); + } else { + addConfigAccount(updatedAccount); + } - if (makeDefault && name) { - setConfigAccountAsDefault(name); + if (makeDefault) { + setConfigAccountAsDefault(updatedAccount.accountId); } return updatedAccount; From 3eef14bb4a655ba76bae4ce1a308309e682a9861 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Thu, 13 Nov 2025 12:22:21 -0500 Subject: [PATCH 47/70] Fix bug with getConfigDefaultAccountIfExists --- config/index.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/config/index.ts b/config/index.ts index 58bcfb49..d7226de2 100644 --- a/config/index.ts +++ b/config/index.ts @@ -210,7 +210,9 @@ export function getConfigDefaultAccount(): HubSpotConfigAccount { let defaultAccountToUse = defaultAccount; - if (globalConfigFileExists()) { + const currentConfigPath = getConfigFilePath(); + const globalConfigPath = getGlobalConfigFilePath(); + if (currentConfigPath === globalConfigPath && globalConfigFileExists()) { const defaultAccountOverrideAccountId = getDefaultAccountOverrideAccountId(); defaultAccountToUse = defaultAccountOverrideAccountId || defaultAccount; @@ -249,7 +251,10 @@ export function getConfigDefaultAccountIfExists(): let defaultAccountToUse = defaultAccount; - if (globalConfigFileExists()) { + // Only check for default account override if we're using the global config + const currentConfigPath = getConfigFilePath(); + const globalConfigPath = getGlobalConfigFilePath(); + if (currentConfigPath === globalConfigPath && globalConfigFileExists()) { const defaultAccountOverrideAccountId = getDefaultAccountOverrideAccountId(); defaultAccountToUse = defaultAccountOverrideAccountId || defaultAccount; From 0dd62dbf5993cd8350db0e2a37fee910d1d0a8d0 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Thu, 13 Nov 2025 13:08:42 -0500 Subject: [PATCH 48/70] Add generic configFileExists --- config/index.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/config/index.ts b/config/index.ts index d7226de2..8032df7f 100644 --- a/config/index.ts +++ b/config/index.ts @@ -2,6 +2,7 @@ import fs from 'fs-extra'; import { ACCOUNT_IDENTIFIERS, + ENVIRONMENT_VARIABLES, HUBSPOT_CONFIG_OPERATIONS, MIN_HTTP_TIMEOUT, } from '../constants/config'; @@ -39,6 +40,14 @@ export function globalConfigFileExists(): boolean { return fs.existsSync(getGlobalConfigFilePath()); } +export function configFileExists(): boolean { + try { + return fs.existsSync(getConfigFilePath()); + } catch (error) { + return false; + } +} + function getConfigDefaultFilePath(): string { const globalConfigFilePath = getGlobalConfigFilePath(); From 3dced3397cb5c75142be8c9c2de894f4cdd399a5 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Mon, 17 Nov 2025 13:49:56 -0500 Subject: [PATCH 49/70] Test some things --- config/index.ts | 6 ++++-- config/utils.ts | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/config/index.ts b/config/index.ts index 8032df7f..a8c26f7b 100644 --- a/config/index.ts +++ b/config/index.ts @@ -2,7 +2,6 @@ import fs from 'fs-extra'; import { ACCOUNT_IDENTIFIERS, - ENVIRONMENT_VARIABLES, HUBSPOT_CONFIG_OPERATIONS, MIN_HTTP_TIMEOUT, } from '../constants/config'; @@ -33,7 +32,10 @@ import { HubSpotConfigError } from '../models/HubSpotConfigError'; import { HUBSPOT_CONFIG_ERROR_TYPES } from '../constants/config'; export function localConfigFileExists(): boolean { - return Boolean(getLocalConfigFilePath()); + const localConfigFilePath = getLocalConfigFilePath(); + console.log('localConfigFilePath', localConfigFilePath); + + return Boolean(localConfigFilePath); } export function globalConfigFileExists(): boolean { diff --git a/config/utils.ts b/config/utils.ts index 54c8d2b7..da784a54 100644 --- a/config/utils.ts +++ b/config/utils.ts @@ -38,6 +38,7 @@ export function getGlobalConfigFilePath(): string { } export function getLocalConfigFilePath(): string | null { + console.log('Search for', DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME); return findup([ DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME, DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME.replace('.yml', '.yaml'), From 8c5ef2566d2759432ae12d6c8649020fa080d604 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Mon, 17 Nov 2025 13:53:55 -0500 Subject: [PATCH 50/70] Fix getLocalConfigFilePath --- config/index.ts | 5 +---- config/utils.ts | 12 +++++++----- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/config/index.ts b/config/index.ts index a8c26f7b..4a7160c3 100644 --- a/config/index.ts +++ b/config/index.ts @@ -32,10 +32,7 @@ import { HubSpotConfigError } from '../models/HubSpotConfigError'; import { HUBSPOT_CONFIG_ERROR_TYPES } from '../constants/config'; export function localConfigFileExists(): boolean { - const localConfigFilePath = getLocalConfigFilePath(); - console.log('localConfigFilePath', localConfigFilePath); - - return Boolean(localConfigFilePath); + return Boolean(getLocalConfigDefaultFilePath()); } export function globalConfigFileExists(): boolean { diff --git a/config/utils.ts b/config/utils.ts index da784a54..0950c76c 100644 --- a/config/utils.ts +++ b/config/utils.ts @@ -38,11 +38,13 @@ export function getGlobalConfigFilePath(): string { } export function getLocalConfigFilePath(): string | null { - console.log('Search for', DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME); - return findup([ - DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME, - DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME.replace('.yml', '.yaml'), - ]); + return findup( + [ + DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME, + DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME.replace('.yml', '.yaml'), + ], + { cwd: getCwd() } + ); } export function getLocalConfigDefaultFilePath(): string { From f8aae4d4ad5e30cb8f3b7c62554a94eae89d4ef8 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Mon, 17 Nov 2025 14:36:35 -0500 Subject: [PATCH 51/70] Fix typo --- config/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/index.ts b/config/index.ts index 4a7160c3..4338326e 100644 --- a/config/index.ts +++ b/config/index.ts @@ -32,7 +32,7 @@ import { HubSpotConfigError } from '../models/HubSpotConfigError'; import { HUBSPOT_CONFIG_ERROR_TYPES } from '../constants/config'; export function localConfigFileExists(): boolean { - return Boolean(getLocalConfigDefaultFilePath()); + return Boolean(getLocalConfigFilePath()); } export function globalConfigFileExists(): boolean { From 221ff2b937650c0a8c3b1829b1aa491834913af6 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Tue, 18 Nov 2025 12:15:50 -0500 Subject: [PATCH 52/70] Update config readme --- config/README.md | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/config/README.md b/config/README.md index 017212fc..4f68dcbd 100644 --- a/config/README.md +++ b/config/README.md @@ -12,22 +12,36 @@ There are a handful of standard config utils that anyone working in this library #### getConfig() -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 +Locates and parses the hubspot config file. This function will automatically find the correct config file using the following criteria: +1. Checks to see if a config was specified via environment variables (see below) +2. If no environment variables are present, looks for a global config file (located at `~/.hscli/config.yml`) +3. If no global config file is found, looks up the file tree for a local config file. +4. If no local config file is found, throws an error -- `USE_ENVIRONTMENT_CONFIG` - load config account from environment variables -- `HUBSPOT_CONFIG_PATH` - specify a path to a specific config file + +##### Custom config location environment variables +- `HUBSPOT_CONFIG_PATH` - specify a file path to a specific config file +- `USE_ENVIRONMENT_HUBSPOT_CONFIG` - load config account from environment variables. The following environment variables are supported: + - `HUBSPOT_PERSONAL_ACCESS_KEY` + - `HUBSPOT_ACCOUNT_ID` + - `HTTP_TIMEOUT` + - `DEFAULT_CMS_PUBLISH_MODE` #### updateConfigAccount() -Safely writes updated values to the `hubspot.config.yml` file. +Safely writes updated values to the HubSpot config file. #### getConfigAccountById() and getConfigAccountByName() -Returns config data for a specific account, given the account's ID or name. +Returns config data for a specific account, given the account's ID or name. Errors if an account is not found. + +#### getAccountIfExist + +Returns config data for a specific account, given either a name or an ID. Returns null without erroring if an account is not found ## Example config -Here is an example of a basic hubspot config file with a single account configured. +Here is an example of a basic HubSpot config file with a single account configured. ```yml defaultPortal: my-hubspot-account From acfee0f480e8fab64301030ff99d5a62ae2d6919 Mon Sep 17 00:00:00 2001 From: joe-yeager Date: Wed, 19 Nov 2025 11:50:31 -0800 Subject: [PATCH 53/70] Fix bug with account creation not adding to config --- lib/personalAccessKey.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/personalAccessKey.ts b/lib/personalAccessKey.ts index 7addff7b..8fafc811 100644 --- a/lib/personalAccessKey.ts +++ b/lib/personalAccessKey.ts @@ -14,7 +14,6 @@ import { Environment } from '../types/Config'; import { getConfigAccountById, getConfigAccountIfExists, - getConfigDefaultAccountIfExists, updateConfigAccount, addConfigAccount, setConfigAccountAsDefault, @@ -183,9 +182,7 @@ export async function updateConfigWithAccessToken( makeDefault = false ): Promise { const { portalId, accessToken, expiresAt, accountType } = token; - const account = name - ? getConfigAccountIfExists(name) - : getConfigDefaultAccountIfExists(); + const account = name ? getConfigAccountIfExists(name) : undefined; const accountEnv = env || account?.env || ENVIRONMENTS.PROD; let parentAccountId; From 062aa9a4ad984e7414d9e211ccf58e9cae1c5baf Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Wed, 19 Nov 2025 15:07:10 -0500 Subject: [PATCH 54/70] More robust error handling --- config/index.ts | 52 ++++++++++++++++++-------- config/utils.ts | 72 ++++++++++++++++++++++++++++-------- constants/config.ts | 3 ++ lang/en.json | 9 ++++- models/HubSpotConfigError.ts | 13 +++++-- 5 files changed, 113 insertions(+), 36 deletions(-) diff --git a/config/index.ts b/config/index.ts index 4338326e..b933eed9 100644 --- a/config/index.ts +++ b/config/index.ts @@ -22,6 +22,8 @@ import { getConfigAccountIndexById, getConfigPathEnvironmentVariables, getConfigAccountByInferredIdentifier, + handleConfigFileSystemError, + doesConfigFileExistAtPath, } from './utils'; import { CMS_PUBLISH_MODE } from '../constants/files'; import { Environment } from '../types/Config'; @@ -36,12 +38,12 @@ export function localConfigFileExists(): boolean { } export function globalConfigFileExists(): boolean { - return fs.existsSync(getGlobalConfigFilePath()); + return doesConfigFileExistAtPath(getGlobalConfigFilePath()); } export function configFileExists(): boolean { try { - return fs.existsSync(getConfigFilePath()); + return doesConfigFileExistAtPath(getConfigFilePath()); } catch (error) { return false; } @@ -50,7 +52,7 @@ export function configFileExists(): boolean { function getConfigDefaultFilePath(): string { const globalConfigFilePath = getGlobalConfigFilePath(); - if (fs.existsSync(globalConfigFilePath)) { + if (doesConfigFileExistAtPath(globalConfigFilePath)) { return globalConfigFilePath; } @@ -161,7 +163,21 @@ export function createEmptyConfigFile(useGlobalConfig = false): void { export function deleteConfigFile(): void { const pathToDelete = getConfigFilePath(); - fs.unlinkSync(pathToDelete); + + try { + fs.unlinkSync(pathToDelete); + } catch (error) { + const { message, type } = handleConfigFileSystemError(error, pathToDelete); + + throw new HubSpotConfigError( + message, + type, + HUBSPOT_CONFIG_OPERATIONS.DELETE, + { + cause: error, + } + ); + } } export function getConfigAccountById(accountId: number): HubSpotConfigAccount { @@ -287,22 +303,26 @@ export function getAllConfigAccounts(): HubSpotConfigAccount[] { } export function getConfigAccountEnvironment( - identifier?: number | string + identifier: number | string ): Environment { - if (identifier) { - const config = getConfig(); + const config = getConfig(); - const account = getConfigAccountByInferredIdentifier( - config.accounts, - identifier - ); + const account = getConfigAccountByInferredIdentifier( + config.accounts, + identifier + ); - if (account) { - return getValidEnv(account.env); - } + if (!account) { + throw new HubSpotConfigError( + i18n('config.getConfigAccountEnvironment.accountNotFound', { + identifier, + }), + HUBSPOT_CONFIG_ERROR_TYPES.ACCOUNT_NOT_FOUND, + HUBSPOT_CONFIG_OPERATIONS.READ + ); } - const defaultAccount = getConfigDefaultAccount(); - return getValidEnv(defaultAccount.env); + + return getValidEnv(account.env); } export function addConfigAccount(accountToAdd: HubSpotConfigAccount): void { diff --git a/config/utils.ts b/config/utils.ts index 0950c76c..5ae5952f 100644 --- a/config/utils.ts +++ b/config/utils.ts @@ -17,7 +17,11 @@ import { OAUTH_AUTH_METHOD, OAUTH_SCOPES, } from '../constants/auth'; -import { HubSpotConfig, DeprecatedHubSpotConfigFields } from '../types/Config'; +import { + HubSpotConfig, + DeprecatedHubSpotConfigFields, + HubSpotConfigErrorType, +} from '../types/Config'; import { FileSystemError } from '../models/FileSystemError'; import { logger } from '../lib/logger'; import { @@ -77,22 +81,32 @@ export function getConfigPathEnvironmentVariables(): { }; } -export function readConfigFile(configPath: string): string { - let source = ''; +export function doesConfigFileExistAtPath(path: string): boolean { + try { + return fs.existsSync(path); + } catch (error) { + const { message, type } = handleConfigFileSystemError(error, path); + throw new HubSpotConfigError( + message, + type, + HUBSPOT_CONFIG_OPERATIONS.READ, + { cause: error } + ); + } +} +export function readConfigFile(configPath: string): string { try { - source = fs.readFileSync(configPath).toString(); + return fs.readFileSync(configPath).toString(); } catch (err) { - throw new FileSystemError( - { cause: err }, - { - filepath: configPath, - operation: 'read', - } + const { message, type } = handleConfigFileSystemError(err, configPath); + throw new HubSpotConfigError( + message, + type, + HUBSPOT_CONFIG_OPERATIONS.READ, + { cause: err } ); } - - return source; } export function removeUndefinedFieldsFromConfigAccount< @@ -170,9 +184,7 @@ export function writeConfigFile( config: HubSpotConfig, configPath: string ): void { - const source = yaml.dump( - JSON.parse(JSON.stringify(formatConfigForWrite(config), null, 2)) - ); + const source = yaml.dump(formatConfigForWrite(config)); try { fs.ensureFileSync(configPath); @@ -203,6 +215,10 @@ function getAccountType(sandboxAccountType?: string): AccountType { export function normalizeParsedConfig( parsedConfig: HubSpotConfig & DeprecatedHubSpotConfigFields ): HubSpotConfig { + if (!parsedConfig.portals && !parsedConfig.accounts) { + parsedConfig.accounts = []; + } + if (parsedConfig.portals) { parsedConfig.accounts = parsedConfig.portals.map(account => { if (account.portalId) { @@ -464,3 +480,29 @@ export function isConfigAccountValid( return valid; } + +export function handleConfigFileSystemError( + error: unknown, + path: string +): { message?: string; type: HubSpotConfigErrorType } { + let message; + let type: HubSpotConfigErrorType = HUBSPOT_CONFIG_ERROR_TYPES.UNKNOWN; + + if (error instanceof Error && 'code' in error) { + if (error.code === 'ENOENT') { + message = i18n( + 'config.utils.handleConfigFileSystemError.configNotFoundError', + { path } + ); + type = HUBSPOT_CONFIG_ERROR_TYPES.CONFIG_NOT_FOUND; + } else if (error.code === 'EACCES') { + message = i18n( + 'config.utils.handleConfigFileSystemError.insufficientPermissionsError', + { path } + ); + type = HUBSPOT_CONFIG_ERROR_TYPES.INSUFFICIENT_PERMISSIONS; + } + } + + return { message, type }; +} diff --git a/constants/config.ts b/constants/config.ts index dd64d1c1..c0f6b836 100644 --- a/constants/config.ts +++ b/constants/config.ts @@ -87,15 +87,18 @@ export const ACCOUNT_IDENTIFIERS = { export const HUBSPOT_CONFIG_ERROR_TYPES = { CONFIG_NOT_FOUND: 'CONFIG_NOT_FOUND', + INSUFFICIENT_PERMISSIONS: 'INSUFFICIENT_PERMISSIONS', ACCOUNT_NOT_FOUND: 'ACCOUNT_NOT_FOUND', NO_DEFAULT_ACCOUNT: 'NO_DEFAULT_ACCOUNT', INVALID_ENVIRONMENT_VARIABLES: 'ENVIRONMENT_VARIABLES', YAML_PARSING: 'YAML_PARSING', INVALID_ACCOUNT: 'INVALID_ACCOUNT', INVALID_FIELD: 'INVALID_FIELD', + UNKNOWN: 'UNKNOWN', } as const; export const HUBSPOT_CONFIG_OPERATIONS = { READ: 'READ', WRITE: 'WRITE', + DELETE: 'DELETE', } as const; diff --git a/lang/en.json b/lang/en.json index 15e8bc63..ef0230a8 100644 --- a/lang/en.json +++ b/lang/en.json @@ -291,7 +291,14 @@ "updateDefaultCmsPublishMode": { "invalidCmsPublishMode": "Error updating config default CMS publish mode: CMS publish can only be set to 'draft' or 'publish'" }, + "getConfigAccountEnvironment": { + "accountNotFound": "Attempted to get environment for account with identifier {{ identifier }}, but that account was not found in config" + }, "utils": { + "handleConfigFileSystemError": { + "configNotFoundError": "No config file found at {{ path }}.", + "insufficientPermissionsError": "Insufficient permissions to access config file at {{ path }}" + }, "isConfigAccountValid": { "missingAccount": "Invalid config: at least one account in config is missing data", "missingAuthType": "Invalid config: account {{ accountId }} has no authType", @@ -382,7 +389,7 @@ } }, "HubSpotConfigError": { - "baseMessage": "An error occurred while {{ operation }} your HubSpot config {{ configType }}: {{ message }}" + "baseMessage": "An error occurred while {{ operation }} your HubSpot config {{ configType }}{{ message }}" } }, "utils": { diff --git a/models/HubSpotConfigError.ts b/models/HubSpotConfigError.ts index f761b5d9..9b6cf8ac 100644 --- a/models/HubSpotConfigError.ts +++ b/models/HubSpotConfigError.ts @@ -10,6 +10,12 @@ import { i18n } from '../utils/lang'; const NAME = 'HubSpotConfigError'; +const OPERATION_TEXT = { + [HUBSPOT_CONFIG_OPERATIONS.READ]: 'reading', + [HUBSPOT_CONFIG_OPERATIONS.WRITE]: 'writing to', + [HUBSPOT_CONFIG_OPERATIONS.DELETE]: 'deleting', +}; + function isEnvironmentError(type: HubSpotConfigErrorType): boolean { return type === HUBSPOT_CONFIG_ERROR_TYPES.INVALID_ENVIRONMENT_VARIABLES; } @@ -19,7 +25,7 @@ export class HubSpotConfigError extends Error { public operation: HubSpotConfigOperation; constructor( - message: string, + message: string | undefined, type: HubSpotConfigErrorType, operation: HubSpotConfigOperation, options?: ErrorOptions @@ -28,12 +34,11 @@ export class HubSpotConfigError extends Error { ? 'environment variables' : 'file'; - const operationText = - operation === HUBSPOT_CONFIG_OPERATIONS.WRITE ? 'writing to' : 'reading'; + const operationText = OPERATION_TEXT[operation]; const withBaseMessage = i18n('models.HubSpotConfigError.baseMessage', { configType, - message, + message: message ? `: ${message}` : '', operation: operationText, }); From eaf7883c4d9d01547e3be37666eb3a7b336fe103 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Wed, 19 Nov 2025 16:25:10 -0500 Subject: [PATCH 55/70] Fix updateConfigWithPersonalAccessKey again --- .npmrc | 1 + config/__tests__/config.test.ts | 6 ------ config/__tests__/utils.test.ts | 5 +++-- lib/__tests__/personalAccessKey.test.ts | 11 ++++++++--- lib/personalAccessKey.ts | 2 +- 5 files changed, 13 insertions(+), 12 deletions(-) create mode 100644 .npmrc diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..214c29d1 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +registry=https://registry.npmjs.org/ diff --git a/config/__tests__/config.test.ts b/config/__tests__/config.test.ts index 237d193f..b74ed2f9 100644 --- a/config/__tests__/config.test.ts +++ b/config/__tests__/config.test.ts @@ -314,12 +314,6 @@ describe('config/index', () => { expect(getConfigAccountEnvironment(123)).toEqual('qa'); }); - - it('returns default account environment when no identifier', () => { - mockConfig(); - - expect(getConfigAccountEnvironment()).toEqual('qa'); - }); }); describe('addConfigAccount()', () => { diff --git a/config/__tests__/utils.test.ts b/config/__tests__/utils.test.ts index 65a9715e..37594218 100644 --- a/config/__tests__/utils.test.ts +++ b/config/__tests__/utils.test.ts @@ -15,6 +15,7 @@ import { isConfigAccountValid, getAccountIdentifierAndType, } from '../utils'; +import { HubSpotConfigError } from '../../models/HubSpotConfigError'; import { getCwd } from '../../lib/path'; import { DeprecatedHubSpotConfigAccountFields, @@ -195,12 +196,12 @@ describe('config/utils', () => { expect(result).toBe('config contents'); }); - it('throws FileSystemError on read failure', () => { + it('throws HubSpotConfigError on read failure', () => { mockFs.readFileSync.mockImplementation(() => { throw new Error('Read error'); }); - expect(() => readConfigFile('test')).toThrow(FileSystemError); + expect(() => readConfigFile('test')).toThrow(HubSpotConfigError); }); }); diff --git a/lib/__tests__/personalAccessKey.test.ts b/lib/__tests__/personalAccessKey.test.ts index cf330cce..b9f2c652 100644 --- a/lib/__tests__/personalAccessKey.test.ts +++ b/lib/__tests__/personalAccessKey.test.ts @@ -657,7 +657,7 @@ describe('lib/personalAccessKey', () => { ); }); - it('uses existing account name when name not provided', async () => { + it('uses existing account name when updating by name', async () => { const existingAccount = { accountId: 123, name: 'existing-name', @@ -671,7 +671,7 @@ describe('lib/personalAccessKey', () => { }, }, }; - getConfigDefaultAccountIfExists.mockReturnValue(existingAccount); + getConfigAccountIfExists.mockReturnValue(existingAccount); const freshAccessToken = 'fresh-token'; fetchAccessToken.mockResolvedValue( @@ -689,7 +689,12 @@ describe('lib/personalAccessKey', () => { const token = await getAccessToken('pak_123', ENVIRONMENTS.QA, 123); - await updateConfigWithAccessToken(token, 'pak_123', ENVIRONMENTS.QA); + await updateConfigWithAccessToken( + token, + 'pak_123', + ENVIRONMENTS.QA, + 'existing-name' + ); expect(updateConfigAccount).toHaveBeenCalledWith( expect.objectContaining({ diff --git a/lib/personalAccessKey.ts b/lib/personalAccessKey.ts index 8fafc811..4e3c812b 100644 --- a/lib/personalAccessKey.ts +++ b/lib/personalAccessKey.ts @@ -182,7 +182,7 @@ export async function updateConfigWithAccessToken( makeDefault = false ): Promise { const { portalId, accessToken, expiresAt, accountType } = token; - const account = name ? getConfigAccountIfExists(name) : undefined; + const account = getConfigAccountIfExists(portalId); const accountEnv = env || account?.env || ENVIRONMENTS.PROD; let parentAccountId; From 5f8df4e09f1a838d2e2b12308479925cdf17aad6 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Wed, 19 Nov 2025 17:24:10 -0500 Subject: [PATCH 56/70] only delete config file if empty --- config/__tests__/config.test.ts | 17 +++++++++++++---- config/index.ts | 6 ++++-- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/config/__tests__/config.test.ts b/config/__tests__/config.test.ts index b74ed2f9..9c2b8526 100644 --- a/config/__tests__/config.test.ts +++ b/config/__tests__/config.test.ts @@ -9,7 +9,7 @@ import { getConfig, isConfigValid, createEmptyConfigFile, - deleteConfigFile, + deleteConfigFileIfEmpty, getConfigAccountById, getConfigAccountByName, getConfigDefaultAccount, @@ -240,13 +240,22 @@ describe('config/index', () => { }); }); - describe('deleteConfigFile()', () => { - it('deletes the config file', () => { + describe('deleteConfigFileIfEmpty()', () => { + it('deletes the config file if it is empty', () => { mockFs.existsSync.mockReturnValue(true); - deleteConfigFile(); + mockFs.readFileSync.mockReturnValueOnce(''); + deleteConfigFileIfEmpty(); expect(mockFs.unlinkSync).toHaveBeenCalledWith(getConfigFilePath()); }); + + it('does not delete the config file if it is not empty', () => { + mockFs.existsSync.mockReturnValue(true); + mockFs.readFileSync.mockReturnValueOnce('test-config-content'); + deleteConfigFileIfEmpty(); + + expect(mockFs.unlinkSync).not.toHaveBeenCalled(); + }); }); describe('getConfigAccountById()', () => { diff --git a/config/index.ts b/config/index.ts index b933eed9..fd501b6d 100644 --- a/config/index.ts +++ b/config/index.ts @@ -161,11 +161,13 @@ export function createEmptyConfigFile(useGlobalConfig = false): void { writeConfigFile({ accounts: [] }, pathToWrite); } -export function deleteConfigFile(): void { +export function deleteConfigFileIfEmpty(): void { const pathToDelete = getConfigFilePath(); try { - fs.unlinkSync(pathToDelete); + if (fs.readFileSync(pathToDelete).length === 0) { + fs.unlinkSync(pathToDelete); + } } catch (error) { const { message, type } = handleConfigFileSystemError(error, pathToDelete); From 56ace63813241b310ddb84360ad3018a4686aa6c Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Wed, 19 Nov 2025 17:42:24 -0500 Subject: [PATCH 57/70] Fix deleteConfigFileIfEmpty --- config/index.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/config/index.ts b/config/index.ts index fd501b6d..608a1a6d 100644 --- a/config/index.ts +++ b/config/index.ts @@ -32,6 +32,9 @@ import { getDefaultAccountOverrideAccountId } from './defaultAccountOverride'; import { getValidEnv } from '../lib/environment'; import { HubSpotConfigError } from '../models/HubSpotConfigError'; import { HUBSPOT_CONFIG_ERROR_TYPES } from '../constants/config'; +import { isDeepEqual } from '../lib/isDeepEqual'; + +const EMPTY_CONFIG = { accounts: [] }; export function localConfigFileExists(): boolean { return Boolean(getLocalConfigFilePath()); @@ -158,14 +161,16 @@ export function createEmptyConfigFile(useGlobalConfig = false): void { const pathToWrite = configFilePathFromEnvironment || defaultPath; - writeConfigFile({ accounts: [] }, pathToWrite); + writeConfigFile(EMPTY_CONFIG, pathToWrite); } export function deleteConfigFileIfEmpty(): void { const pathToDelete = getConfigFilePath(); try { - if (fs.readFileSync(pathToDelete).length === 0) { + const config = getConfig(); + + if (isDeepEqual(config, EMPTY_CONFIG)) { fs.unlinkSync(pathToDelete); } } catch (error) { From c1e6e0403b41f6ec5c74e8a500619f5f154c5568 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Thu, 20 Nov 2025 12:16:20 -0500 Subject: [PATCH 58/70] export getLocalConfigFilePath and getGlobalConfigFilePath --- config/__tests__/config.test.ts | 50 +++++++++++++++++++++++++++----- config/__tests__/migrate.test.ts | 10 ++----- config/__tests__/utils.test.ts | 28 ------------------ config/index.ts | 24 ++++++++++++--- config/migrate.ts | 9 ++---- config/utils.ts | 14 --------- 6 files changed, 67 insertions(+), 68 deletions(-) diff --git a/config/__tests__/config.test.ts b/config/__tests__/config.test.ts index 9c2b8526..f2b804bf 100644 --- a/config/__tests__/config.test.ts +++ b/config/__tests__/config.test.ts @@ -24,6 +24,8 @@ import { updateAllowUsageTracking, updateDefaultCmsPublishMode, isConfigFlagEnabled, + getGlobalConfigFilePath, + getLocalConfigFilePathIfExists, } from '../index'; import { HubSpotConfigAccount } from '../../types/Accounts'; import { HubSpotConfig } from '../../types/Config'; @@ -37,13 +39,13 @@ import { OAUTH_AUTH_METHOD, API_KEY_AUTH_METHOD, } from '../../constants/auth'; -import { - getGlobalConfigFilePath, - getLocalConfigDefaultFilePath, - formatConfigForWrite, -} from '../utils'; +import { getLocalConfigDefaultFilePath, formatConfigForWrite } from '../utils'; import { getDefaultAccountOverrideAccountId } from '../defaultAccountOverride'; -import { CONFIG_FLAGS, ENVIRONMENT_VARIABLES } from '../../constants/config'; +import { + CONFIG_FLAGS, + ENVIRONMENT_VARIABLES, + HUBSPOT_CONFIGURATION_FOLDER, +} from '../../constants/config'; import * as utils from '../utils'; import { CmsPublishMode } from '../../types/Files'; @@ -128,6 +130,32 @@ describe('config/index', () => { cleanup(); }); + describe('getGlobalConfigFilePath()', () => { + it('returns the global config file path', () => { + const globalConfigFilePath = getGlobalConfigFilePath(); + expect(globalConfigFilePath).toBeDefined(); + expect(globalConfigFilePath).toContain( + `${HUBSPOT_CONFIGURATION_FOLDER}/config.yml` + ); + }); + }); + + describe('getLocalConfigFilePathIfExists()', () => { + it('returns the nearest config file path', () => { + const mockConfigPath = '/mock/path/hubspot.config.yml'; + mockFindup.mockReturnValue(mockConfigPath); + + const localConfigPath = getLocalConfigFilePathIfExists(); + expect(localConfigPath).toBe(mockConfigPath); + }); + + it('returns null if no config file found', () => { + mockFindup.mockReturnValue(null); + const localConfigPath = getLocalConfigFilePathIfExists(); + expect(localConfigPath).toBeNull(); + }); + }); + describe('localConfigFileExists()', () => { it('returns true when local config exists', () => { mockFindup.mockReturnValueOnce(getLocalConfigDefaultFilePath()); @@ -243,7 +271,10 @@ describe('config/index', () => { describe('deleteConfigFileIfEmpty()', () => { it('deletes the config file if it is empty', () => { mockFs.existsSync.mockReturnValue(true); - mockFs.readFileSync.mockReturnValueOnce(''); + mockFs.readFileSync.mockReturnValueOnce(yaml.dump({ accounts: [] })); + jest + .spyOn(utils, 'parseConfig') + .mockReturnValueOnce({ accounts: [] } as HubSpotConfig); deleteConfigFileIfEmpty(); expect(mockFs.unlinkSync).toHaveBeenCalledWith(getConfigFilePath()); @@ -251,7 +282,10 @@ describe('config/index', () => { it('does not delete the config file if it is not empty', () => { mockFs.existsSync.mockReturnValue(true); - mockFs.readFileSync.mockReturnValueOnce('test-config-content'); + mockFs.readFileSync.mockReturnValueOnce(yaml.dump(CONFIG)); + jest + .spyOn(utils, 'parseConfig') + .mockReturnValueOnce(structuredClone(CONFIG)); deleteConfigFileIfEmpty(); expect(mockFs.unlinkSync).not.toHaveBeenCalled(); diff --git a/config/__tests__/migrate.test.ts b/config/__tests__/migrate.test.ts index 35556cfe..4b7e43cf 100644 --- a/config/__tests__/migrate.test.ts +++ b/config/__tests__/migrate.test.ts @@ -9,11 +9,7 @@ import { mergeConfigAccounts, } from '../migrate'; import { HubSpotConfig } from '../../types/Config'; -import { - getGlobalConfigFilePath, - readConfigFile, - writeConfigFile, -} from '../utils'; +import { readConfigFile, writeConfigFile } from '../utils'; import { DEFAULT_CMS_PUBLISH_MODE, HTTP_TIMEOUT, @@ -25,7 +21,7 @@ import { import { ENVIRONMENTS } from '../../constants/environments'; import { PERSONAL_ACCESS_KEY_AUTH_METHOD } from '../../constants/auth'; import { PersonalAccessKeyConfigAccount } from '../../types/Accounts'; -import { createEmptyConfigFile } from '../index'; +import { createEmptyConfigFile, getGlobalConfigFilePath } from '../index'; jest.mock('fs', () => ({ ...jest.requireActual('fs'), @@ -36,12 +32,12 @@ jest.mock('../utils', () => ({ ...jest.requireActual('../utils'), readConfigFile: jest.fn(), writeConfigFile: jest.fn(), - getGlobalConfigFilePath: jest.fn(), })); jest.mock('../index', () => ({ ...jest.requireActual('../index'), createEmptyConfigFile: jest.fn(), + getGlobalConfigFilePath: jest.fn(), })); describe('config/migrate', () => { diff --git a/config/__tests__/utils.test.ts b/config/__tests__/utils.test.ts index 37594218..2533cd64 100644 --- a/config/__tests__/utils.test.ts +++ b/config/__tests__/utils.test.ts @@ -1,8 +1,6 @@ import findup from 'findup-sync'; import fs from 'fs-extra'; import { - getGlobalConfigFilePath, - getLocalConfigFilePath, getLocalConfigDefaultFilePath, getConfigPathEnvironmentVariables, readConfigFile, @@ -134,32 +132,6 @@ describe('config/utils', () => { 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'; diff --git a/config/index.ts b/config/index.ts index 608a1a6d..248bf00d 100644 --- a/config/index.ts +++ b/config/index.ts @@ -1,7 +1,10 @@ import fs from 'fs-extra'; +import findup from 'findup-sync'; import { ACCOUNT_IDENTIFIERS, + DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME, + GLOBAL_CONFIG_PATH, HUBSPOT_CONFIG_OPERATIONS, MIN_HTTP_TIMEOUT, } from '../constants/config'; @@ -10,8 +13,6 @@ import { HubSpotConfig, ConfigFlag } from '../types/Config'; import { CmsPublishMode } from '../types/Files'; import { logger } from '../lib/logger'; import { - getGlobalConfigFilePath, - getLocalConfigFilePath, readConfigFile, parseConfig, buildConfigFromEnvironment, @@ -33,11 +34,26 @@ import { getValidEnv } from '../lib/environment'; import { HubSpotConfigError } from '../models/HubSpotConfigError'; import { HUBSPOT_CONFIG_ERROR_TYPES } from '../constants/config'; import { isDeepEqual } from '../lib/isDeepEqual'; +import { getCwd } from '../lib/path'; const EMPTY_CONFIG = { accounts: [] }; +export function getGlobalConfigFilePath(): string { + return GLOBAL_CONFIG_PATH; +} + +export function getLocalConfigFilePathIfExists(): string | null { + return findup( + [ + DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME, + DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME.replace('.yml', '.yaml'), + ], + { cwd: getCwd() } + ); +} + export function localConfigFileExists(): boolean { - return Boolean(getLocalConfigFilePath()); + return Boolean(getLocalConfigFilePathIfExists()); } export function globalConfigFileExists(): boolean { @@ -59,7 +75,7 @@ function getConfigDefaultFilePath(): string { return globalConfigFilePath; } - const localConfigFilePath = getLocalConfigFilePath(); + const localConfigFilePath = getLocalConfigFilePathIfExists(); if (!localConfigFilePath) { throw new HubSpotConfigError( diff --git a/config/migrate.ts b/config/migrate.ts index 42b40bbf..5f1de14b 100644 --- a/config/migrate.ts +++ b/config/migrate.ts @@ -1,7 +1,7 @@ import fs from 'fs'; import { HubSpotConfig } from '../types/Config'; -import { createEmptyConfigFile } from './index'; +import { createEmptyConfigFile, getGlobalConfigFilePath } from './index'; import { DEFAULT_CMS_PUBLISH_MODE, HTTP_TIMEOUT, @@ -12,12 +12,7 @@ import { AUTO_OPEN_BROWSER, ALLOW_AUTO_UPDATES, } from '../constants/config'; -import { - getGlobalConfigFilePath, - parseConfig, - readConfigFile, - writeConfigFile, -} from './utils'; +import { parseConfig, readConfigFile, writeConfigFile } from './utils'; import { ValueOf } from '../types/Utils'; export function getConfigAtPath(path: string): HubSpotConfig { diff --git a/config/utils.ts b/config/utils.ts index 5ae5952f..cd5085cb 100644 --- a/config/utils.ts +++ b/config/utils.ts @@ -37,20 +37,6 @@ import { i18n } from '../utils/lang'; import { ValueOf } from '../types/Utils'; import { HubSpotConfigError } from '../models/HubSpotConfigError'; -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'), - ], - { cwd: getCwd() } - ); -} - export function getLocalConfigDefaultFilePath(): string { return `${getCwd()}/${DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME}`; } From 0eb86da701444d74e143b79906ce50adde13ce2e Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Thu, 20 Nov 2025 13:26:08 -0500 Subject: [PATCH 59/70] Fix some migrate bugs --- config/migrate.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/config/migrate.ts b/config/migrate.ts index 5f1de14b..ae921169 100644 --- a/config/migrate.ts +++ b/config/migrate.ts @@ -11,9 +11,11 @@ import { DEFAULT_ACCOUNT, AUTO_OPEN_BROWSER, ALLOW_AUTO_UPDATES, + ARCHIVED_HUBSPOT_CONFIG_YAML_FILE_NAME, } from '../constants/config'; import { parseConfig, readConfigFile, writeConfigFile } from './utils'; import { ValueOf } from '../types/Utils'; +import path from 'path'; export function getConfigAtPath(path: string): HubSpotConfig { const configFileSource = readConfigFile(path); @@ -25,7 +27,6 @@ export function migrateConfigAtPath(path: string): void { createEmptyConfigFile(true); const configToMigrate = getConfigAtPath(path); writeConfigFile(configToMigrate, getGlobalConfigFilePath()); - fs.unlinkSync(path); } export type ConflictProperty = { @@ -146,3 +147,12 @@ export function mergeConfigAccounts( writeConfigFile(configWithMergedAccounts, getGlobalConfigFilePath()); return { configWithMergedAccounts, skippedAccountIds }; } + +export function archiveConfigAtPath(configPath: string): void { + const dir = path.dirname(configPath); + const archivedConfigPath = path.join( + dir, + ARCHIVED_HUBSPOT_CONFIG_YAML_FILE_NAME + ); + fs.renameSync(configPath, archivedConfigPath); +} From ff10935380891f8c5b47d679d0e23b740d3f3e33 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Thu, 20 Nov 2025 13:29:11 -0500 Subject: [PATCH 60/70] Fix tests --- config/__tests__/migrate.test.ts | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/config/__tests__/migrate.test.ts b/config/__tests__/migrate.test.ts index 4b7e43cf..e5f4c25f 100644 --- a/config/__tests__/migrate.test.ts +++ b/config/__tests__/migrate.test.ts @@ -7,6 +7,7 @@ import { migrateConfigAtPath, mergeConfigProperties, mergeConfigAccounts, + archiveConfigAtPath, } from '../migrate'; import { HubSpotConfig } from '../../types/Config'; import { readConfigFile, writeConfigFile } from '../utils'; @@ -17,6 +18,7 @@ import { HTTP_USE_LOCALHOST, ALLOW_USAGE_TRACKING, DEFAULT_ACCOUNT, + ARCHIVED_HUBSPOT_CONFIG_YAML_FILE_NAME, } from '../../constants/config'; import { ENVIRONMENTS } from '../../constants/environments'; import { PERSONAL_ACCESS_KEY_AUTH_METHOD } from '../../constants/auth'; @@ -26,6 +28,7 @@ import { createEmptyConfigFile, getGlobalConfigFilePath } from '../index'; jest.mock('fs', () => ({ ...jest.requireActual('fs'), unlinkSync: jest.fn(), + renameSync: jest.fn(), })); jest.mock('../utils', () => ({ @@ -100,7 +103,6 @@ describe('config/migrate', () => { mockConfig, mockGlobalConfigPath ); - expect(fs.unlinkSync).toHaveBeenCalledWith(mockConfigPath); }); }); @@ -451,4 +453,20 @@ describe('config/migrate', () => { ); }); }); + + describe('archiveConfigAtPath', () => { + const mockRenameSync = fs.renameSync as jest.Mock; + + it('should rename config file to archived config file', () => { + const configPath = '/home/user/project/hubspot.config.yml'; + const expectedArchivedPath = `/home/user/project/${ARCHIVED_HUBSPOT_CONFIG_YAML_FILE_NAME}`; + + archiveConfigAtPath(configPath); + + expect(mockRenameSync).toHaveBeenCalledWith( + configPath, + expectedArchivedPath + ); + }); + }); }); From 2eacdae6e10eaacafbd531b2a0d49402299e2c20 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Thu, 20 Nov 2025 15:43:54 -0500 Subject: [PATCH 61/70] Fix migrate conflict bug --- config/migrate.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/config/migrate.ts b/config/migrate.ts index ae921169..9d2989d1 100644 --- a/config/migrate.ts +++ b/config/migrate.ts @@ -88,7 +88,11 @@ export function mergeConfigProperties( ] as const; propertiesToCheck.forEach(prop => { - if (toConfig[prop] !== undefined && toConfig[prop] !== fromConfig[prop]) { + if ( + toConfig[prop] !== undefined && + fromConfig[prop] !== undefined && + toConfig[prop] !== fromConfig[prop] + ) { conflicts.push({ property: prop, oldValue: fromConfig[prop], From fd5c85eb00bb1a6623b43b46f61ea18d2b4d4488 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Thu, 20 Nov 2025 15:54:08 -0500 Subject: [PATCH 62/70] Fix bug in updateConfigWithAccessToken --- lib/personalAccessKey.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/personalAccessKey.ts b/lib/personalAccessKey.ts index 4e3c812b..59fb8700 100644 --- a/lib/personalAccessKey.ts +++ b/lib/personalAccessKey.ts @@ -228,12 +228,12 @@ export async function updateConfigWithAccessToken( accountId: portalId, accountType, personalAccessKey, - name: name || account?.name || token.hubName, + name: name || account?.name, authType: PERSONAL_ACCESS_KEY_AUTH_METHOD.value, auth: { tokenInfo: { accessToken, expiresAt } }, parentAccountId, env: accountEnv, - }; + } as PersonalAccessKeyConfigAccount; // Account may temporarily not have a name before prompted to add one in the CLI // Add new account if it doesn't exist, otherwise update existing account if (account) { From 67d76307fbc8c10362baf7ac2799b75e84b1ce1b Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Thu, 20 Nov 2025 16:39:52 -0500 Subject: [PATCH 63/70] Update config validation --- config/__tests__/config.test.ts | 21 +++++++--- config/__tests__/utils.test.ts | 38 +++++++++++++---- config/index.ts | 43 +++++++++++-------- config/utils.ts | 52 ++++++++++++----------- lang/en.json | 4 +- lib/__tests__/personalAccessKey.test.ts | 55 ++++++++++++++++++++++--- types/Config.ts | 5 +++ 7 files changed, 154 insertions(+), 64 deletions(-) diff --git a/config/__tests__/config.test.ts b/config/__tests__/config.test.ts index f2b804bf..dc45377e 100644 --- a/config/__tests__/config.test.ts +++ b/config/__tests__/config.test.ts @@ -7,7 +7,7 @@ import { globalConfigFileExists, getConfigFilePath, getConfig, - isConfigValid, + validateConfig, createEmptyConfigFile, deleteConfigFileIfEmpty, getConfigAccountById, @@ -48,6 +48,7 @@ import { } from '../../constants/config'; import * as utils from '../utils'; import { CmsPublishMode } from '../../types/Files'; +import { i18n } from '../../utils/lang'; jest.mock('findup-sync'); jest.mock('../../lib/path'); @@ -226,23 +227,33 @@ describe('config/index', () => { }); }); - describe('isConfigValid()', () => { + describe('validateConfig()', () => { it('returns true for valid config', () => { mockConfig(); - expect(isConfigValid()).toBe(true); + expect(validateConfig()).toEqual({ isValid: true, errors: [] }); }); it('returns false for config with no accounts', () => { mockConfig({ accounts: [] }); - expect(isConfigValid()).toBe(false); + expect(validateConfig()).toEqual({ + isValid: false, + errors: [i18n('config.validateConfig.missingAccounts')], + }); }); it('returns false for config with duplicate account ids', () => { mockConfig({ accounts: [PAK_ACCOUNT, PAK_ACCOUNT] }); - expect(isConfigValid()).toBe(false); + expect(validateConfig()).toEqual({ + isValid: false, + errors: [ + i18n('config.validateConfig.duplicateAccountIds', { + accountId: PAK_ACCOUNT.accountId, + }), + ], + }); }); }); diff --git a/config/__tests__/utils.test.ts b/config/__tests__/utils.test.ts index 2533cd64..805d3878 100644 --- a/config/__tests__/utils.test.ts +++ b/config/__tests__/utils.test.ts @@ -10,7 +10,7 @@ import { buildConfigFromEnvironment, getConfigAccountByIdentifier, getConfigAccountIndexById, - isConfigAccountValid, + validateConfigAccount, getAccountIdentifierAndType, } from '../utils'; import { HubSpotConfigError } from '../../models/HubSpotConfigError'; @@ -36,6 +36,7 @@ import { OAUTH_AUTH_METHOD, API_KEY_AUTH_METHOD, } from '../../constants/auth'; +import { i18n } from '../../utils/lang'; jest.mock('findup-sync'); jest.mock('../../lib/path'); @@ -319,32 +320,51 @@ describe('config/utils', () => { }); }); - describe('isConfigAccountValid()', () => { + describe('validateConfigAccount()', () => { it('validates personal access key account', () => { - expect(isConfigAccountValid(PAK_ACCOUNT)).toBe(true); + expect(validateConfigAccount(PAK_ACCOUNT)).toEqual({ + isValid: true, + errors: [], + }); }); it('validates OAuth account', () => { - expect(isConfigAccountValid(OAUTH_ACCOUNT)).toBe(true); + expect(validateConfigAccount(OAUTH_ACCOUNT)).toEqual({ + isValid: true, + errors: [], + }); }); it('validates API key account', () => { - expect(isConfigAccountValid(API_KEY_ACCOUNT)).toBe(true); + expect(validateConfigAccount(API_KEY_ACCOUNT)).toEqual({ + isValid: true, + errors: [], + }); }); it('returns false for invalid account', () => { expect( - isConfigAccountValid({ + validateConfigAccount({ ...PAK_ACCOUNT, personalAccessKey: undefined, }) - ).toBe(false); + ).toEqual({ + isValid: false, + errors: [ + i18n('config.utils.validateConfigAccount.missingPersonalAccessKey', { + accountId: PAK_ACCOUNT.accountId, + }), + ], + }); expect( - isConfigAccountValid({ + validateConfigAccount({ ...PAK_ACCOUNT, accountId: undefined, }) - ).toBe(false); + ).toEqual({ + isValid: false, + errors: [i18n('config.utils.validateConfigAccount.missingAccountId')], + }); }); }); diff --git a/config/index.ts b/config/index.ts index 248bf00d..5d472019 100644 --- a/config/index.ts +++ b/config/index.ts @@ -9,7 +9,11 @@ import { MIN_HTTP_TIMEOUT, } from '../constants/config'; import { HubSpotConfigAccount } from '../types/Accounts'; -import { HubSpotConfig, ConfigFlag } from '../types/Config'; +import { + HubSpotConfig, + ConfigFlag, + HubSpotConfigValidationResult, +} from '../types/Config'; import { CmsPublishMode } from '../types/Files'; import { logger } from '../lib/logger'; import { @@ -19,7 +23,7 @@ import { writeConfigFile, getLocalConfigDefaultFilePath, getConfigAccountByIdentifier, - isConfigAccountValid, + validateConfigAccount, getConfigAccountIndexById, getConfigPathEnvironmentVariables, getConfigAccountByInferredIdentifier, @@ -121,52 +125,55 @@ export function getConfig(): HubSpotConfig { } } -export function isConfigValid(): boolean { +export function validateConfig(): HubSpotConfigValidationResult { const config = getConfig(); if (config.accounts.length === 0) { - logger.debug(i18n('config.isConfigValid.missingAccounts')); - return false; + return { + isValid: false, + errors: [i18n('config.validateConfig.missingAccounts')], + }; } const accountIdsMap: { [key: number]: boolean } = {}; const accountNamesMap: { [key: string]: boolean } = {}; - return config.accounts.every(account => { - if (!isConfigAccountValid(account)) { - return false; + const validationErrors: string[] = []; + + config.accounts.forEach(account => { + const accountValidationResult = validateConfigAccount(account); + if (!accountValidationResult.isValid) { + return accountValidationResult; } if (accountIdsMap[account.accountId]) { - logger.debug( - i18n('config.isConfigValid.duplicateAccountIds', { + validationErrors.push( + i18n('config.validateConfig.duplicateAccountIds', { accountId: account.accountId, }) ); - return false; } if (account.name) { if (accountNamesMap[account.name.toLowerCase()]) { logger.debug( - i18n('config.isConfigValid.duplicateAccountNames', { + i18n('config.validateConfig.duplicateAccountNames', { accountName: account.name, }) ); - return false; } if (/\s+/.test(account.name)) { logger.debug( - i18n('config.isConfigValid.invalidAccountName', { + i18n('config.validateConfig.invalidAccountName', { accountName: account.name, }) ); - return false; } accountNamesMap[account.name] = true; } accountIdsMap[account.accountId] = true; - return true; }); + + return { isValid: validationErrors.length === 0, errors: validationErrors }; } export function createEmptyConfigFile(useGlobalConfig = false): void { @@ -349,7 +356,7 @@ export function getConfigAccountEnvironment( } export function addConfigAccount(accountToAdd: HubSpotConfigAccount): void { - if (!isConfigAccountValid(accountToAdd)) { + if (!validateConfigAccount(accountToAdd)) { throw new HubSpotConfigError( i18n('config.addConfigAccount.invalidAccount'), HUBSPOT_CONFIG_ERROR_TYPES.INVALID_ACCOUNT, @@ -383,7 +390,7 @@ export function addConfigAccount(accountToAdd: HubSpotConfigAccount): void { export function updateConfigAccount( updatedAccount: HubSpotConfigAccount ): void { - if (!isConfigAccountValid(updatedAccount)) { + if (!validateConfigAccount(updatedAccount)) { throw new HubSpotConfigError( i18n('config.updateConfigAccount.invalidAccount', { name: updatedAccount.name, diff --git a/config/utils.ts b/config/utils.ts index cd5085cb..4f5015a5 100644 --- a/config/utils.ts +++ b/config/utils.ts @@ -21,6 +21,7 @@ import { HubSpotConfig, DeprecatedHubSpotConfigFields, HubSpotConfigErrorType, + HubSpotConfigValidationResult, } from '../types/Config'; import { FileSystemError } from '../models/FileSystemError'; import { logger } from '../lib/logger'; @@ -403,37 +404,40 @@ export function getConfigAccountIndexById( return accounts.findIndex(account => account.accountId === id); } -export function isConfigAccountValid( +export function validateConfigAccount( account: Partial -): boolean { +): HubSpotConfigValidationResult { + const validationErrors = []; if (!account || typeof account !== 'object') { - logger.debug(i18n('config.utils.isConfigAccountValid.missingAccount')); - return false; + validationErrors.push( + i18n('config.utils.validateConfigAccount.missingAccount') + ); + return { isValid: false, errors: validationErrors }; } if (!account.accountId) { - logger.debug(i18n('config.utils.isConfigAccountValid.missingAccountId')); - return false; + validationErrors.push( + i18n('config.utils.validateConfigAccount.missingAccountId') + ); + return { isValid: false, errors: validationErrors }; } if (!account.authType) { - logger.debug( - i18n('config.utils.isConfigAccountValid.missingAuthType', { + validationErrors.push( + i18n('config.utils.validateConfigAccount.missingAuthType', { accountId: account.accountId, }) ); - return false; + return { isValid: false, errors: validationErrors }; } - let valid = false; - if (account.authType === PERSONAL_ACCESS_KEY_AUTH_METHOD.value) { - valid = + const isValidPersonalAccessKeyAccount = 'personalAccessKey' in account && Boolean(account.personalAccessKey); - if (!valid) { - logger.debug( - i18n('config.utils.isConfigAccountValid.missingPersonalAccessKey', { + if (!isValidPersonalAccessKeyAccount) { + validationErrors.push( + i18n('config.utils.validateConfigAccount.missingPersonalAccessKey', { accountId: account.accountId, }) ); @@ -441,11 +445,11 @@ export function isConfigAccountValid( } if (account.authType === OAUTH_AUTH_METHOD.value) { - valid = 'auth' in account && Boolean(account.auth); + const isValidOAuthAccount = 'auth' in account && Boolean(account.auth); - if (!valid) { - logger.debug( - i18n('config.utils.isConfigAccountValid.missingAuth', { + if (!isValidOAuthAccount) { + validationErrors.push( + i18n('config.utils.validateConfigAccount.missingAuth', { accountId: account.accountId, }) ); @@ -453,18 +457,18 @@ export function isConfigAccountValid( } if (account.authType === API_KEY_AUTH_METHOD.value) { - valid = 'apiKey' in account && Boolean(account.apiKey); + const isValidAPIKeyAccount = 'apiKey' in account && Boolean(account.apiKey); - if (!valid) { - logger.debug( - i18n('config.utils.isConfigAccountValid.missingApiKey', { + if (!isValidAPIKeyAccount) { + validationErrors.push( + i18n('config.utils.validateConfigAccount.missingApiKey', { accountId: account.accountId, }) ); } } - return valid; + return { isValid: validationErrors.length === 0, errors: validationErrors }; } export function handleConfigFileSystemError( diff --git a/lang/en.json b/lang/en.json index ef0230a8..9e59435d 100644 --- a/lang/en.json +++ b/lang/en.json @@ -251,7 +251,7 @@ "error": "No config file found.", "errorWithPath": "No config file found at {{ path }}." }, - "isConfigValid": { + "validateConfig": { "missingAccounts": "Invalid config: no accounts found", "duplicateAccountIds": "Invalid config: multiple accounts with accountId: {{ accountId }}", "duplicateAccountNames": "Invalid config: multiple accounts with name: {{ accountName }}", @@ -299,7 +299,7 @@ "configNotFoundError": "No config file found at {{ path }}.", "insufficientPermissionsError": "Insufficient permissions to access config file at {{ path }}" }, - "isConfigAccountValid": { + "validateConfigAccount": { "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", diff --git a/lib/__tests__/personalAccessKey.test.ts b/lib/__tests__/personalAccessKey.test.ts index b9f2c652..f736f71e 100644 --- a/lib/__tests__/personalAccessKey.test.ts +++ b/lib/__tests__/personalAccessKey.test.ts @@ -628,9 +628,8 @@ describe('lib/personalAccessKey', () => { ); }); - it('uses token hubName when name not provided and no existing account', async () => { + it('creates account with undefined name when name not provided and no existing account', async () => { getConfigAccountIfExists.mockReturnValue(undefined); - getConfigDefaultAccountIfExists.mockReturnValue(undefined); const freshAccessToken = 'fresh-token'; fetchAccessToken.mockResolvedValue( @@ -652,15 +651,16 @@ describe('lib/personalAccessKey', () => { expect(addConfigAccount).toHaveBeenCalledWith( expect.objectContaining({ - name: 'hub-from-token', + accountId: 123, + name: undefined, }) ); }); - it('uses existing account name when updating by name', async () => { + it('uses provided name when updating existing account found by portalId', async () => { const existingAccount = { accountId: 123, - name: 'existing-name', + name: 'old-name', authType: 'personalaccesskey' as const, personalAccessKey: 'old-key', env: ENVIRONMENTS.PROD, @@ -693,11 +693,54 @@ describe('lib/personalAccessKey', () => { token, 'pak_123', ENVIRONMENTS.QA, - 'existing-name' + 'new-name' + ); + + expect(updateConfigAccount).toHaveBeenCalledWith( + expect.objectContaining({ + accountId: 123, + name: 'new-name', + }) + ); + }); + + it('uses existing account name when no name provided and account exists', async () => { + const existingAccount = { + accountId: 123, + name: 'existing-name', + authType: 'personalaccesskey' as const, + personalAccessKey: 'old-key', + env: ENVIRONMENTS.PROD, + auth: { + tokenInfo: { + accessToken: 'old-token', + expiresAt: moment().add(1, 'hours').toISOString(), + }, + }, + }; + getConfigAccountIfExists.mockReturnValue(existingAccount); + + const freshAccessToken = 'fresh-token'; + fetchAccessToken.mockResolvedValue( + mockAxiosResponse({ + oauthAccessToken: freshAccessToken, + expiresAtMillis: moment().add(1, 'hours').valueOf(), + encodedOAuthRefreshToken: 'let-me-in-13', + scopeGroups: ['content'], + hubId: 123, + userId: 456, + hubName: 'hub-from-token', + accountType: HUBSPOT_ACCOUNT_TYPES.STANDARD, + }) ); + const token = await getAccessToken('pak_123', ENVIRONMENTS.QA, 123); + + await updateConfigWithAccessToken(token, 'pak_123', ENVIRONMENTS.QA); + expect(updateConfigAccount).toHaveBeenCalledWith( expect.objectContaining({ + accountId: 123, name: 'existing-name', }) ); diff --git a/types/Config.ts b/types/Config.ts index c6f92122..d0e64681 100644 --- a/types/Config.ts +++ b/types/Config.ts @@ -49,3 +49,8 @@ export type HubSpotState = { export type HubSpotConfigErrorType = ValueOf; export type HubSpotConfigOperation = ValueOf; + +export type HubSpotConfigValidationResult = { + isValid: boolean; + errors: Array; +}; From 5f50750b5850c9a38d6f164f956eb9d5e930b869 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Thu, 20 Nov 2025 16:48:20 -0500 Subject: [PATCH 64/70] Fix bug with validateConfig --- config/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/index.ts b/config/index.ts index 5d472019..b9a02f14 100644 --- a/config/index.ts +++ b/config/index.ts @@ -143,7 +143,7 @@ export function validateConfig(): HubSpotConfigValidationResult { config.accounts.forEach(account => { const accountValidationResult = validateConfigAccount(account); if (!accountValidationResult.isValid) { - return accountValidationResult; + validationErrors.push(...accountValidationResult.errors); } if (accountIdsMap[account.accountId]) { validationErrors.push( From f9c7313e96e5d1629a3f7483b62ce9a1a2c6c0e7 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Thu, 20 Nov 2025 16:53:28 -0500 Subject: [PATCH 65/70] Update error copy --- lang/en.json | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lang/en.json b/lang/en.json index 9e59435d..6aa10f12 100644 --- a/lang/en.json +++ b/lang/en.json @@ -252,10 +252,10 @@ "errorWithPath": "No config file found at {{ path }}." }, "validateConfig": { - "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" + "missingAccounts": "No accounts found", + "duplicateAccountIds": "Multiple accounts with accountId: {{ accountId }}", + "duplicateAccountNames": "Multiple accounts with name: {{ accountName }}", + "invalidAccountName": "Account name {{ accountName }} contains spaces" }, "getConfigAccountById": { "error": "No account with id {{ accountId }} exists in config" @@ -300,12 +300,12 @@ "insufficientPermissionsError": "Insufficient permissions to access config file at {{ path }}" }, "validateConfigAccount": { - "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" + "missingAccount": "At least one account in config is missing data", + "missingAuthType": "Account {{ accountId }} has no authType", + "missingAccountId": "At least one account in config is missing accountId", + "missingApiKey": "Account {{ accountId }} has authType of apikey but is missing the apiKey field", + "missingAuth": "Account {{ accountId }} has authtype of oauth2 but is missing auth data", + "missingPersonalAccessKey": "Account {{ accountId }} has authType of personalAccessKey but is missing the personalAccessKey field" }, "getConfigPathEnvironmentVariables": { "invalidEnvironmentVariables": "USE_ENVIRONMENT_HUBSPOT_CONFIG and HUBSPOT_CONFIG_PATH cannot both be set simultaneously" From 6166648f3e0d1f294b1b0aeafff405302716976f Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Fri, 21 Nov 2025 10:33:17 -0500 Subject: [PATCH 66/70] Fix bug in validateConfig --- config/index.ts | 4 ++-- config/utils.ts | 3 --- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/config/index.ts b/config/index.ts index b9a02f14..94f4a4aa 100644 --- a/config/index.ts +++ b/config/index.ts @@ -154,14 +154,14 @@ export function validateConfig(): HubSpotConfigValidationResult { } if (account.name) { if (accountNamesMap[account.name.toLowerCase()]) { - logger.debug( + validationErrors.push( i18n('config.validateConfig.duplicateAccountNames', { accountName: account.name, }) ); } if (/\s+/.test(account.name)) { - logger.debug( + validationErrors.push( i18n('config.validateConfig.invalidAccountName', { accountName: account.name, }) diff --git a/config/utils.ts b/config/utils.ts index 4f5015a5..38f08c98 100644 --- a/config/utils.ts +++ b/config/utils.ts @@ -1,13 +1,11 @@ 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, HUBSPOT_CONFIG_ERROR_TYPES, HUBSPOT_CONFIG_OPERATIONS, } from '../constants/config'; @@ -24,7 +22,6 @@ import { HubSpotConfigValidationResult, } from '../types/Config'; import { FileSystemError } from '../models/FileSystemError'; -import { logger } from '../lib/logger'; import { HubSpotConfigAccount, OAuthConfigAccount, From d1c303e1f9082afe5b5b6cf7ff150d93744ca97c Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Fri, 21 Nov 2025 11:17:16 -0500 Subject: [PATCH 67/70] Make getConfigAccountByInferredIdentifier more robust --- config/__tests__/utils.test.ts | 5 +---- config/index.ts | 2 +- config/utils.ts | 18 +++++++++++++++++- lang/en.json | 2 +- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/config/__tests__/utils.test.ts b/config/__tests__/utils.test.ts index 805d3878..758dd521 100644 --- a/config/__tests__/utils.test.ts +++ b/config/__tests__/utils.test.ts @@ -26,10 +26,7 @@ import { DeprecatedHubSpotConfigFields, HubSpotConfig, } from '../../types/Config'; -import { - ENVIRONMENT_VARIABLES, - HUBSPOT_CONFIGURATION_FOLDER, -} from '../../constants/config'; +import { ENVIRONMENT_VARIABLES } from '../../constants/config'; import { FileSystemError } from '../../models/FileSystemError'; import { PERSONAL_ACCESS_KEY_AUTH_METHOD, diff --git a/config/index.ts b/config/index.ts index 94f4a4aa..ed4b0123 100644 --- a/config/index.ts +++ b/config/index.ts @@ -433,7 +433,7 @@ export function setConfigAccountAsDefault(identifier: number | string): void { if (!account) { throw new HubSpotConfigError( i18n('config.setConfigAccountAsDefault.accountNotFound', { - accountId: identifier, + identifier, }), HUBSPOT_CONFIG_ERROR_TYPES.ACCOUNT_NOT_FOUND, HUBSPOT_CONFIG_OPERATIONS.WRITE diff --git a/config/utils.ts b/config/utils.ts index 38f08c98..d2dc3aa9 100644 --- a/config/utils.ts +++ b/config/utils.ts @@ -391,7 +391,23 @@ export function getConfigAccountByInferredIdentifier( ): HubSpotConfigAccount | undefined { const { identifier, identifierType } = getAccountIdentifierAndType(accountIdentifier); - return accounts.find(account => account[identifierType] === identifier); + + const account = getConfigAccountByIdentifier( + accounts, + identifierType, + identifier + ); + + if (account) { + return account; + } + + // Fallback to handle accounts with numbers as names + return getConfigAccountByIdentifier( + accounts, + ACCOUNT_IDENTIFIERS.NAME, + String(accountIdentifier) + ); } export function getConfigAccountIndexById( diff --git a/lang/en.json b/lang/en.json index 6aa10f12..2a1e0240 100644 --- a/lang/en.json +++ b/lang/en.json @@ -276,7 +276,7 @@ "accountNotFound": "Attempting to update account with id {{ id }}, but that account was not found in config" }, "setConfigAccountAsDefault": { - "accountNotFound": "Attempted to set account with id {{ accountId }} as default, but that account was not found in config" + "accountNotFound": "Attempted to set account with identifier {{ identifier }} as default, but that account was not found in config" }, "renameConfigAccount": { "accountNotFound": "Attempted to rename account with name {{ currentName }}, but that account was not found in config", From 0306b9d8cf77d9383a286fd5078782d46c70b23a Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Fri, 21 Nov 2025 14:25:13 -0500 Subject: [PATCH 68/70] Allow custom cwd for getLocalConfigFilePath --- config/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/index.ts b/config/index.ts index ed4b0123..717d88fd 100644 --- a/config/index.ts +++ b/config/index.ts @@ -46,13 +46,13 @@ export function getGlobalConfigFilePath(): string { return GLOBAL_CONFIG_PATH; } -export function getLocalConfigFilePathIfExists(): string | null { +export function getLocalConfigFilePathIfExists(cwd?: string): string | null { return findup( [ DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME, DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME.replace('.yml', '.yaml'), ], - { cwd: getCwd() } + { cwd: cwd || getCwd() } ); } From 0e17f07e5f2d1d08a8255f42096bfb414fa34f25 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Fri, 21 Nov 2025 14:30:05 -0500 Subject: [PATCH 69/70] Don't need custom cwd --- config/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/index.ts b/config/index.ts index 717d88fd..ed4b0123 100644 --- a/config/index.ts +++ b/config/index.ts @@ -46,13 +46,13 @@ export function getGlobalConfigFilePath(): string { return GLOBAL_CONFIG_PATH; } -export function getLocalConfigFilePathIfExists(cwd?: string): string | null { +export function getLocalConfigFilePathIfExists(): string | null { return findup( [ DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME, DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME.replace('.yml', '.yaml'), ], - { cwd: cwd || getCwd() } + { cwd: getCwd() } ); } From 478523006603d76b357f9f8abceaf73df8a5ddc5 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Fri, 21 Nov 2025 14:37:24 -0500 Subject: [PATCH 70/70] Fix tests --- config/__tests__/config.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config/__tests__/config.test.ts b/config/__tests__/config.test.ts index dc45377e..b6062924 100644 --- a/config/__tests__/config.test.ts +++ b/config/__tests__/config.test.ts @@ -252,6 +252,9 @@ describe('config/index', () => { i18n('config.validateConfig.duplicateAccountIds', { accountId: PAK_ACCOUNT.accountId, }), + i18n('config.validateConfig.duplicateAccountNames', { + accountName: PAK_ACCOUNT.name, + }), ], }); });