Skip to content

Commit 63dd276

Browse files
feat: Convert deprecated configs back to old format before writing (#341)
* Write to deprecated configs with old formatting * Actually use the reformatted config * fix tests --------- Co-authored-by: Branden Rodgers <branden@rodgersworld.com>
1 parent 6f678ec commit 63dd276

File tree

3 files changed

+374
-9
lines changed

3 files changed

+374
-9
lines changed

config/__tests__/config.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ describe('config/index', () => {
277277

278278
expect(mockFs.writeFileSync).toHaveBeenCalledWith(
279279
getLocalConfigDefaultFilePath(),
280-
yaml.dump({ accounts: [] })
280+
yaml.dump({ portals: [] })
281281
);
282282
});
283283
});

config/__tests__/utils.test.ts

Lines changed: 317 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
1-
import findup from 'findup-sync';
21
import fs from 'fs-extra';
32
import {
43
getLocalConfigDefaultFilePath,
54
getConfigPathEnvironmentVariables,
5+
doesConfigFileExistAtPath,
66
readConfigFile,
77
removeUndefinedFieldsFromConfigAccount,
8+
formatConfigForWrite,
89
writeConfigFile,
10+
parseConfig,
911
normalizeParsedConfig,
12+
convertToDeprecatedConfig,
1013
buildConfigFromEnvironment,
14+
getAccountIdentifierAndType,
1115
getConfigAccountByIdentifier,
16+
getConfigAccountByInferredIdentifier,
1217
getConfigAccountIndexById,
1318
validateConfigAccount,
14-
getAccountIdentifierAndType,
19+
handleConfigFileSystemError,
1520
} from '../utils';
1621
import { HubSpotConfigError } from '../../models/HubSpotConfigError';
1722
import { getCwd } from '../../lib/path';
@@ -39,7 +44,6 @@ jest.mock('findup-sync');
3944
jest.mock('../../lib/path');
4045
jest.mock('fs-extra');
4146

42-
const mockFindup = findup as jest.MockedFunction<typeof findup>;
4347
const mockCwd = getCwd as jest.MockedFunction<typeof getCwd>;
4448
const mockFs = fs as jest.Mocked<typeof fs>;
4549

@@ -224,6 +228,127 @@ describe('config/utils', () => {
224228
});
225229
});
226230

231+
describe('convertToDeprecatedConfig()', () => {
232+
it('converts account fields to portal fields', () => {
233+
const deprecated = convertToDeprecatedConfig(CONFIG);
234+
235+
expect(deprecated.portals).toBeDefined();
236+
expect(deprecated.accounts).toBeUndefined();
237+
expect(deprecated.portals).toHaveLength(1);
238+
expect(deprecated.portals![0]).toHaveProperty('portalId', 123);
239+
expect(deprecated.portals![0]).not.toHaveProperty('accountId');
240+
});
241+
242+
it('converts defaultAccount to defaultPortal', () => {
243+
const deprecated = convertToDeprecatedConfig(CONFIG);
244+
245+
expect(deprecated.defaultPortal).toBe('test-account');
246+
expect(deprecated.defaultAccount).toBeUndefined();
247+
});
248+
249+
it('preserves personal access key account fields', () => {
250+
const deprecated = convertToDeprecatedConfig(CONFIG);
251+
252+
expect(deprecated.portals![0]).toMatchObject({
253+
name: 'test-account',
254+
portalId: 123,
255+
authType: 'personalaccesskey',
256+
personalAccessKey: 'test-key',
257+
env: 'qa',
258+
});
259+
});
260+
261+
it('preserves OAuth account fields', () => {
262+
const oauthConfig = {
263+
...CONFIG,
264+
accounts: [OAUTH_ACCOUNT],
265+
defaultAccount: 123,
266+
};
267+
const deprecated = convertToDeprecatedConfig(oauthConfig);
268+
269+
expect(deprecated.portals![0]).toMatchObject({
270+
name: '123',
271+
portalId: 123,
272+
authType: 'oauth2',
273+
auth: {
274+
clientId: 'test-client-id',
275+
clientSecret: 'test-client-secret',
276+
tokenInfo: {
277+
refreshToken: 'test-refresh-token',
278+
},
279+
scopes: ['content', 'hubdb', 'files'],
280+
},
281+
});
282+
});
283+
284+
it('preserves API key account fields', () => {
285+
const apiKeyConfig = {
286+
...CONFIG,
287+
accounts: [API_KEY_ACCOUNT],
288+
defaultAccount: 123,
289+
};
290+
const deprecated = convertToDeprecatedConfig(apiKeyConfig);
291+
292+
expect(deprecated.portals![0]).toMatchObject({
293+
name: '123',
294+
portalId: 123,
295+
authType: 'apikey',
296+
apiKey: 'test-api-key',
297+
});
298+
});
299+
300+
it('handles multiple accounts', () => {
301+
const multiAccountConfig = {
302+
...CONFIG,
303+
accounts: [PAK_ACCOUNT, OAUTH_ACCOUNT, API_KEY_ACCOUNT],
304+
defaultAccount: 123,
305+
};
306+
const deprecated = convertToDeprecatedConfig(multiAccountConfig);
307+
308+
expect(deprecated.portals).toHaveLength(3);
309+
expect(deprecated.portals![0].portalId).toBe(123);
310+
expect(deprecated.portals![1].portalId).toBe(123);
311+
expect(deprecated.portals![2].portalId).toBe(123);
312+
});
313+
314+
it('handles config without defaultAccount', () => {
315+
const configWithoutDefault = {
316+
...CONFIG,
317+
defaultAccount: undefined,
318+
};
319+
const deprecated = convertToDeprecatedConfig(configWithoutDefault);
320+
321+
expect(deprecated.defaultPortal).toBeUndefined();
322+
expect(deprecated.defaultAccount).toBeUndefined();
323+
});
324+
325+
it('preserves all other config fields', () => {
326+
const deprecated = convertToDeprecatedConfig(CONFIG);
327+
328+
expect(deprecated.defaultCmsPublishMode).toBe('publish');
329+
expect(deprecated.httpTimeout).toBe(1000);
330+
expect(deprecated.httpUseLocalhost).toBe(true);
331+
expect(deprecated.allowUsageTracking).toBe(true);
332+
});
333+
334+
it('does not mutate the original config', () => {
335+
const testConfig = structuredClone(CONFIG);
336+
337+
convertToDeprecatedConfig(testConfig);
338+
339+
// Verify original structure is preserved
340+
expect(testConfig.accounts).toBeDefined();
341+
expect(testConfig.accounts).toHaveLength(1);
342+
expect(testConfig.accounts[0]).toHaveProperty('accountId', 123);
343+
expect(testConfig.defaultAccount).toBe(123);
344+
345+
// Verify deprecated fields were not added to original
346+
expect(testConfig).not.toHaveProperty('portals');
347+
expect(testConfig).not.toHaveProperty('defaultPortal');
348+
expect(testConfig.accounts[0]).not.toHaveProperty('portalId');
349+
});
350+
});
351+
227352
describe('buildConfigFromEnvironment()', () => {
228353
it('builds personal access key config', () => {
229354
process.env[ENVIRONMENT_VARIABLES.HUBSPOT_PERSONAL_ACCESS_KEY] =
@@ -385,4 +510,193 @@ describe('config/utils', () => {
385510
expect(identifierType).toBe('accountId');
386511
});
387512
});
513+
514+
describe('doesConfigFileExistAtPath()', () => {
515+
it('returns true when file exists', () => {
516+
mockFs.existsSync.mockReturnValue(true);
517+
expect(doesConfigFileExistAtPath('/path/to/config.yml')).toBe(true);
518+
});
519+
520+
it('returns false when file does not exist', () => {
521+
mockFs.existsSync.mockReturnValue(false);
522+
expect(doesConfigFileExistAtPath('/path/to/config.yml')).toBe(false);
523+
});
524+
525+
it('throws HubSpotConfigError on error', () => {
526+
mockFs.existsSync.mockImplementation(() => {
527+
throw new Error('Permission denied');
528+
});
529+
expect(() => doesConfigFileExistAtPath('/path/to/config.yml')).toThrow(
530+
HubSpotConfigError
531+
);
532+
});
533+
});
534+
535+
describe('formatConfigForWrite()', () => {
536+
it('formats config with consistent field order', () => {
537+
const formattedConfig = formatConfigForWrite(CONFIG);
538+
const keys = Object.keys(formattedConfig);
539+
expect(keys[0]).toBe('defaultAccount');
540+
expect(keys[keys.length - 1]).toBe('accounts');
541+
});
542+
543+
it('removes undefined fields from accounts', () => {
544+
const configWithUndefined = {
545+
...CONFIG,
546+
accounts: [
547+
{
548+
...PAK_ACCOUNT,
549+
someUndefinedField: undefined,
550+
} as any,
551+
],
552+
};
553+
const formattedConfig = formatConfigForWrite(configWithUndefined);
554+
expect(formattedConfig.accounts[0]).not.toHaveProperty(
555+
'someUndefinedField'
556+
);
557+
});
558+
559+
it('preserves all account types', () => {
560+
const multiAccountConfig = {
561+
...CONFIG,
562+
accounts: [PAK_ACCOUNT, OAUTH_ACCOUNT, API_KEY_ACCOUNT],
563+
};
564+
const formatted = formatConfigForWrite(multiAccountConfig);
565+
expect(formatted.accounts).toHaveLength(3);
566+
expect(formatted.accounts[0].authType).toBe('personalaccesskey');
567+
expect(formatted.accounts[1].authType).toBe('oauth2');
568+
expect(formatted.accounts[2].authType).toBe('apikey');
569+
});
570+
});
571+
572+
describe('parseConfig()', () => {
573+
it('parses valid YAML config', () => {
574+
const yamlConfig = `
575+
accounts:
576+
- name: test-account
577+
accountId: 123
578+
authType: personalaccesskey
579+
personalAccessKey: test-key
580+
env: qa
581+
auth:
582+
tokenInfo: {}
583+
accountType: STANDARD
584+
defaultAccount: 123
585+
defaultCmsPublishMode: publish
586+
httpTimeout: 1000
587+
httpUseLocalhost: true
588+
allowUsageTracking: true
589+
`;
590+
const parsed = parseConfig(yamlConfig, '/path/to/config.yml');
591+
expect(parsed).toEqual(CONFIG);
592+
});
593+
594+
it('throws HubSpotConfigError on invalid YAML', () => {
595+
const invalidYaml = 'invalid: yaml: content: [';
596+
expect(() => parseConfig(invalidYaml, '/path/to/config.yml')).toThrow(
597+
HubSpotConfigError
598+
);
599+
});
600+
601+
it('normalizes deprecated config format', () => {
602+
const deprecatedYaml = `
603+
portals:
604+
- name: test-account
605+
portalId: 123
606+
authType: personalaccesskey
607+
personalAccessKey: test-key
608+
env: qa
609+
auth:
610+
tokenInfo: {}
611+
defaultPortal: test-account
612+
defaultMode: publish
613+
httpTimeout: 1000
614+
httpUseLocalhost: true
615+
allowUsageTracking: true
616+
`;
617+
const parsed = parseConfig(deprecatedYaml, '/path/to/config.yml');
618+
expect(parsed).toEqual(CONFIG);
619+
});
620+
});
621+
622+
describe('getConfigAccountByInferredIdentifier()', () => {
623+
it('finds account by numeric accountId', () => {
624+
const account = getConfigAccountByInferredIdentifier(
625+
CONFIG.accounts,
626+
123
627+
);
628+
expect(account).toEqual(PAK_ACCOUNT);
629+
});
630+
631+
it('finds account by string accountId', () => {
632+
const account = getConfigAccountByInferredIdentifier(
633+
CONFIG.accounts,
634+
'123'
635+
);
636+
expect(account).toEqual(PAK_ACCOUNT);
637+
});
638+
639+
it('finds account by name', () => {
640+
const account = getConfigAccountByInferredIdentifier(
641+
CONFIG.accounts,
642+
'test-account'
643+
);
644+
expect(account).toEqual(PAK_ACCOUNT);
645+
});
646+
647+
it('handles accounts with numeric names as fallback', () => {
648+
const accountWithNumericName = {
649+
...PAK_ACCOUNT,
650+
name: '456',
651+
accountId: 789,
652+
};
653+
const accounts = [accountWithNumericName];
654+
const account = getConfigAccountByInferredIdentifier(accounts, '456');
655+
expect(account).toEqual(accountWithNumericName);
656+
});
657+
658+
it('returns undefined when account not found', () => {
659+
const account = getConfigAccountByInferredIdentifier(
660+
CONFIG.accounts,
661+
999
662+
);
663+
expect(account).toBeUndefined();
664+
});
665+
});
666+
667+
describe('handleConfigFileSystemError()', () => {
668+
it('handles ENOENT error', () => {
669+
const error = Object.assign(new Error('File not found'), {
670+
code: 'ENOENT',
671+
});
672+
const { message, type } = handleConfigFileSystemError(
673+
error,
674+
'/path/to/config.yml'
675+
);
676+
expect(message).toContain('No config file found');
677+
expect(type).toBe('CONFIG_NOT_FOUND');
678+
});
679+
680+
it('handles EACCES error', () => {
681+
const error = Object.assign(new Error('Permission denied'), {
682+
code: 'EACCES',
683+
});
684+
const { message, type } = handleConfigFileSystemError(
685+
error,
686+
'/path/to/config.yml'
687+
);
688+
expect(message).toContain('Insufficient permissions');
689+
expect(type).toBe('INSUFFICIENT_PERMISSIONS');
690+
});
691+
692+
it('handles unknown errors', () => {
693+
const error = new Error('Unknown error');
694+
const { message, type } = handleConfigFileSystemError(
695+
error,
696+
'/path/to/config.yml'
697+
);
698+
expect(message).toBeUndefined();
699+
expect(type).toBe('UNKNOWN');
700+
});
701+
});
388702
});

0 commit comments

Comments
 (0)