-
Notifications
You must be signed in to change notification settings - Fork 63
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'main' of github.com:HubSpot/hubspot-cli into jy/fix-gh-…
…rate-limit
- Loading branch information
Showing
4 changed files
with
286 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,221 @@ | ||
import { getAccountId, getConfigAccounts } from '@hubspot/local-dev-lib/config'; | ||
import { logger } from '@hubspot/local-dev-lib/logger'; | ||
import { CLIAccount } from '@hubspot/local-dev-lib/types/Accounts'; | ||
import { HubSpotHttpError } from '@hubspot/local-dev-lib/models/HubSpotHttpError'; | ||
import { HUBSPOT_ACCOUNT_TYPES } from '@hubspot/local-dev-lib/constants/config'; | ||
import { fetchDeveloperTestAccounts } from '@hubspot/local-dev-lib/api/developerTestAccounts'; | ||
import * as errorHandlers from '../errorHandlers'; | ||
import { | ||
getHasDevTestAccounts, | ||
handleDeveloperTestAccountCreateError, | ||
validateDevTestAccountUsageLimits, | ||
} from '../developerTestAccounts'; | ||
|
||
jest.mock('@hubspot/local-dev-lib/config'); | ||
jest.mock('@hubspot/local-dev-lib/logger'); | ||
jest.mock('@hubspot/local-dev-lib/api/developerTestAccounts'); | ||
jest.mock('../errorHandlers'); | ||
|
||
const mockedGetAccountId = getAccountId as jest.Mock; | ||
const mockedGetConfigAccounts = getConfigAccounts as jest.Mock; | ||
const mockedFetchDeveloperTestAccounts = | ||
fetchDeveloperTestAccounts as jest.Mock; | ||
|
||
const APP_DEVELOPER_ACCOUNT_1: CLIAccount = { | ||
name: 'app-developer-1', | ||
accountId: 123, | ||
accountType: HUBSPOT_ACCOUNT_TYPES.APP_DEVELOPER, | ||
env: 'prod', | ||
}; | ||
|
||
const APP_DEVELOPER_ACCOUNT_2: CLIAccount = { | ||
name: 'app-developer-2', | ||
accountId: 456, | ||
accountType: HUBSPOT_ACCOUNT_TYPES.APP_DEVELOPER, | ||
env: 'prod', | ||
}; | ||
|
||
const accounts: CLIAccount[] = [ | ||
APP_DEVELOPER_ACCOUNT_1, | ||
APP_DEVELOPER_ACCOUNT_2, | ||
{ | ||
name: 'test-account', | ||
accountId: 789, | ||
parentAccountId: APP_DEVELOPER_ACCOUNT_1.accountId, | ||
accountType: HUBSPOT_ACCOUNT_TYPES.DEVELOPER_TEST, | ||
env: 'prod', | ||
}, | ||
]; | ||
|
||
const makeHubSpotHttpError = ( | ||
message: string, | ||
response: object | ||
): HubSpotHttpError => { | ||
return new HubSpotHttpError(message, { | ||
cause: { isAxiosError: true, response }, | ||
}); | ||
}; | ||
|
||
describe('lib/developerTestAccounts', () => { | ||
describe('getHasDevTestAccounts()', () => { | ||
it('should return true if there are developer test accounts associated with the account', () => { | ||
mockedGetAccountId.mockReturnValueOnce(APP_DEVELOPER_ACCOUNT_1.accountId); | ||
mockedGetConfigAccounts.mockReturnValueOnce(accounts); | ||
|
||
const result = getHasDevTestAccounts(APP_DEVELOPER_ACCOUNT_1); | ||
expect(result).toBe(true); | ||
}); | ||
|
||
it('should return false if there are no developer test accounts associated with the account', () => { | ||
mockedGetAccountId.mockReturnValueOnce(APP_DEVELOPER_ACCOUNT_2.accountId); | ||
mockedGetConfigAccounts.mockReturnValueOnce(accounts); | ||
|
||
const result = getHasDevTestAccounts(APP_DEVELOPER_ACCOUNT_2); | ||
expect(result).toBe(false); | ||
}); | ||
|
||
it('should return false if there are no accounts configured', () => { | ||
mockedGetAccountId.mockReturnValueOnce(APP_DEVELOPER_ACCOUNT_1.accountId); | ||
mockedGetConfigAccounts.mockReturnValueOnce(undefined); | ||
|
||
const result = getHasDevTestAccounts(APP_DEVELOPER_ACCOUNT_1); | ||
expect(result).toBe(false); | ||
}); | ||
}); | ||
|
||
describe('validateDevTestAccountUsageLimits()', () => { | ||
afterEach(() => { | ||
mockedGetAccountId.mockRestore(); | ||
mockedFetchDeveloperTestAccounts.mockRestore(); | ||
}); | ||
|
||
it('should return null if the account id is not found', async () => { | ||
mockedGetAccountId.mockReturnValueOnce(undefined); | ||
|
||
const result = await validateDevTestAccountUsageLimits( | ||
APP_DEVELOPER_ACCOUNT_1 | ||
); | ||
expect(result).toBe(null); | ||
}); | ||
|
||
it('should return null if there is no developer test account data', async () => { | ||
mockedGetAccountId.mockReturnValueOnce(APP_DEVELOPER_ACCOUNT_1.accountId); | ||
mockedFetchDeveloperTestAccounts.mockResolvedValueOnce({ | ||
data: null, | ||
}); | ||
|
||
const result = await validateDevTestAccountUsageLimits( | ||
APP_DEVELOPER_ACCOUNT_1 | ||
); | ||
expect(result).toBe(null); | ||
}); | ||
|
||
it('should return the test account data if the account has not reached the limit', async () => { | ||
mockedGetAccountId.mockReturnValueOnce(APP_DEVELOPER_ACCOUNT_1.accountId); | ||
const testAccountData = { | ||
maxTestPortals: 10, | ||
results: [], | ||
}; | ||
mockedFetchDeveloperTestAccounts.mockResolvedValueOnce({ | ||
data: testAccountData, | ||
}); | ||
|
||
const result = await validateDevTestAccountUsageLimits( | ||
APP_DEVELOPER_ACCOUNT_1 | ||
); | ||
expect(result).toEqual(expect.objectContaining(testAccountData)); | ||
}); | ||
|
||
it('should throw an error if the account has reached the limit', () => { | ||
mockedGetAccountId.mockReturnValueOnce(APP_DEVELOPER_ACCOUNT_1.accountId); | ||
mockedFetchDeveloperTestAccounts.mockResolvedValueOnce({ | ||
data: { | ||
maxTestPortals: 0, | ||
results: [{}], | ||
}, | ||
}); | ||
|
||
expect( | ||
validateDevTestAccountUsageLimits(APP_DEVELOPER_ACCOUNT_1) | ||
).rejects.toThrow(); | ||
}); | ||
}); | ||
|
||
describe('handleDeveloperTestAccountCreateError()', () => { | ||
let loggerErrorSpy: jest.SpyInstance; | ||
let logErrorSpy: jest.SpyInstance; | ||
|
||
beforeEach(() => { | ||
loggerErrorSpy = jest.spyOn(logger, 'error'); | ||
logErrorSpy = jest.spyOn(errorHandlers, 'logError'); | ||
}); | ||
|
||
afterEach(() => { | ||
loggerErrorSpy.mockRestore(); | ||
logErrorSpy.mockRestore(); | ||
}); | ||
|
||
it('should log and throw an error if the account is missing the required scopes', () => { | ||
const missingScopesError = makeHubSpotHttpError('Missing scopes error', { | ||
status: 403, | ||
data: { | ||
message: 'Missing scopes error', | ||
category: 'MISSING_SCOPES', | ||
}, | ||
}); | ||
|
||
expect(() => | ||
handleDeveloperTestAccountCreateError( | ||
missingScopesError, | ||
APP_DEVELOPER_ACCOUNT_1.accountId, | ||
'prod', | ||
10 | ||
) | ||
).toThrow('Missing scopes error'); | ||
expect(loggerErrorSpy).toHaveBeenCalled(); | ||
}); | ||
|
||
it('should log and throw an error if the account is missing the required scopes', () => { | ||
const portalLimitReachedError = makeHubSpotHttpError( | ||
'Portal limit reached error', | ||
{ | ||
status: 400, | ||
data: { | ||
message: 'Portal limit reached error', | ||
errorType: 'TEST_PORTAL_LIMIT_REACHED', | ||
}, | ||
} | ||
); | ||
|
||
expect(() => | ||
handleDeveloperTestAccountCreateError( | ||
portalLimitReachedError, | ||
APP_DEVELOPER_ACCOUNT_1.accountId, | ||
'prod', | ||
10 | ||
) | ||
).toThrow('Portal limit reached error'); | ||
expect(loggerErrorSpy).toHaveBeenCalled(); | ||
}); | ||
|
||
it('should log a generic error message for an unknown error type', () => { | ||
const someUnknownError = makeHubSpotHttpError('Some unknown error', { | ||
status: 400, | ||
data: { | ||
message: 'Some unknown error', | ||
category: 'SOME_UNKNOWN_ERROR', | ||
}, | ||
}); | ||
|
||
expect(() => | ||
handleDeveloperTestAccountCreateError( | ||
someUnknownError, | ||
APP_DEVELOPER_ACCOUNT_1.accountId, | ||
'prod', | ||
10 | ||
) | ||
).toThrow('Some unknown error'); | ||
expect(logErrorSpy).toHaveBeenCalled(); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import { fetchEnabledFeatures } from '@hubspot/local-dev-lib/api/localDevAuth'; | ||
import { hasFeature } from '../hasFeature'; | ||
|
||
jest.mock('@hubspot/local-dev-lib/api/localDevAuth'); | ||
|
||
const mockedFetchEnabledFeatures = fetchEnabledFeatures as jest.Mock; | ||
|
||
describe('lib/hasFeature', () => { | ||
describe('hasFeature()', () => { | ||
const accountId = 123; | ||
|
||
beforeEach(() => { | ||
mockedFetchEnabledFeatures.mockResolvedValueOnce({ | ||
data: { | ||
enabledFeatures: { | ||
'feature-1': true, | ||
'feature-2': false, | ||
'feature-3': true, | ||
}, | ||
}, | ||
}); | ||
}); | ||
|
||
it('should return true if the feature is enabled', async () => { | ||
const result = await hasFeature(accountId, 'feature-1'); | ||
expect(result).toBe(true); | ||
}); | ||
|
||
it('should return false if the feature is not enabled', async () => { | ||
const result = await hasFeature(accountId, 'feature-2'); | ||
expect(result).toBe(false); | ||
}); | ||
|
||
it('should return false if the feature is not present', async () => { | ||
const result = await hasFeature(accountId, 'feature-4'); | ||
expect(result).toBe(false); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { hasFlag } from '../hasFlag'; | ||
|
||
const argvWithFlag = ['hs', 'command', '--test']; | ||
const argvWithoutFlag = ['hs', 'command']; | ||
|
||
describe('lib/hasFlag', () => { | ||
describe('hasFlag()', () => { | ||
it('should return true if the flag is present', () => { | ||
const flag = hasFlag('test', argvWithFlag); | ||
expect(flag).toBe(true); | ||
}); | ||
|
||
it('should return false if argv is an empty array', () => { | ||
const flag = hasFlag('test', []); | ||
expect(flag).toBe(false); | ||
}); | ||
|
||
it('should return false if the flag is not present', () => { | ||
const flag = hasFlag('test', argvWithoutFlag); | ||
expect(flag).toBe(false); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters