Skip to content

Commit 047a7ef

Browse files
chore: Unit tests for sandboxes.ts (#1366)
1 parent 56511be commit 047a7ef

File tree

2 files changed

+248
-1
lines changed

2 files changed

+248
-1
lines changed

lib/__tests__/sandboxes.test.ts

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
import { logger } from '@hubspot/local-dev-lib/logger';
2+
import { HubSpotHttpError } from '@hubspot/local-dev-lib/models/HubSpotHttpError';
3+
import { getSandboxUsageLimits } from '@hubspot/local-dev-lib/api/sandboxHubs';
4+
import { fetchTypes } from '@hubspot/local-dev-lib/api/sandboxSync';
5+
import { getAccountId, getConfigAccounts } from '@hubspot/local-dev-lib/config';
6+
import { HUBSPOT_ACCOUNT_TYPES } from '@hubspot/local-dev-lib/constants/config';
7+
import { Environment } from '@hubspot/local-dev-lib/types/Config';
8+
import {
9+
getSandboxTypeAsString,
10+
getHasSandboxesByType,
11+
getAvailableSyncTypes,
12+
validateSandboxUsageLimits,
13+
handleSandboxCreateError,
14+
} from '../sandboxes';
15+
16+
jest.mock('@hubspot/local-dev-lib/logger');
17+
jest.mock('@hubspot/local-dev-lib/api/sandboxHubs');
18+
jest.mock('@hubspot/local-dev-lib/api/sandboxSync');
19+
jest.mock('@hubspot/local-dev-lib/config');
20+
21+
const mockedGetAccountId = getAccountId as jest.Mock;
22+
const mockedGetSandboxUsageLimits = getSandboxUsageLimits as jest.Mock;
23+
const mockedFetchTypes = fetchTypes as jest.Mock;
24+
const mockedGetConfigAccounts = getConfigAccounts as jest.Mock;
25+
const mockedLogger = logger as jest.Mocked<typeof logger>;
26+
27+
describe('lib/sandboxes', () => {
28+
describe('getSandboxTypeAsString()', () => {
29+
it('returns "development" for development sandbox type', () => {
30+
expect(
31+
getSandboxTypeAsString(HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX)
32+
).toBe('development');
33+
});
34+
35+
it('returns "standard" for standard sandbox type', () => {
36+
expect(
37+
getSandboxTypeAsString(HUBSPOT_ACCOUNT_TYPES.STANDARD_SANDBOX)
38+
).toBe('standard');
39+
});
40+
41+
it('returns "standard" for undefined input', () => {
42+
expect(getSandboxTypeAsString(undefined)).toBe('standard');
43+
});
44+
});
45+
46+
describe('getHasSandboxesByType()', () => {
47+
const mockParentAccount = {
48+
name: 'Parent Account',
49+
portalId: 123,
50+
authType: undefined,
51+
accountType: HUBSPOT_ACCOUNT_TYPES.STANDARD_SANDBOX,
52+
env: 'qa' as Environment,
53+
};
54+
55+
it('returns true when sandbox of specified type exists', () => {
56+
mockedGetAccountId.mockReturnValue(mockParentAccount.portalId);
57+
mockedGetConfigAccounts.mockReturnValue([
58+
mockParentAccount,
59+
{
60+
...mockParentAccount,
61+
parentAccountId: 123,
62+
accountType: HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX,
63+
},
64+
]);
65+
66+
expect(
67+
getHasSandboxesByType(
68+
mockParentAccount,
69+
HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX
70+
)
71+
).toBe(true);
72+
});
73+
74+
it('returns false when no sandbox of specified type exists', () => {
75+
mockedGetConfigAccounts.mockReturnValue([mockParentAccount]);
76+
77+
expect(
78+
getHasSandboxesByType(
79+
mockParentAccount,
80+
HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX
81+
)
82+
).toBe(false);
83+
});
84+
});
85+
86+
describe('getAvailableSyncTypes()', () => {
87+
const mockParentAccount = {
88+
name: 'Parent Account',
89+
portalId: 123,
90+
env: 'qa' as Environment,
91+
};
92+
93+
const mockChildAccount = {
94+
...mockParentAccount,
95+
portalId: 456,
96+
};
97+
98+
it('returns available sync types when fetch is successful', async () => {
99+
const mockSyncTypes = [{ name: 'type1' }, { name: 'type2' }];
100+
mockedGetAccountId
101+
.mockReturnValue(mockParentAccount.portalId)
102+
.mockReturnValue(mockChildAccount.portalId);
103+
mockedFetchTypes.mockResolvedValue({
104+
data: { results: mockSyncTypes },
105+
});
106+
107+
const result = await getAvailableSyncTypes(
108+
mockParentAccount,
109+
mockChildAccount
110+
);
111+
112+
expect(result).toEqual([{ type: 'type1' }, { type: 'type2' }]);
113+
});
114+
115+
it('throws error when sync types fetch fails', async () => {
116+
mockedFetchTypes.mockResolvedValue({ data: { results: null } });
117+
118+
await expect(
119+
getAvailableSyncTypes(mockParentAccount, mockChildAccount)
120+
).rejects.toThrow(/Unable to fetch available sandbox sync types/);
121+
});
122+
});
123+
124+
describe('validateSandboxUsageLimits()', () => {
125+
const mockAccount = {
126+
name: 'Test Account',
127+
portalId: 123,
128+
authType: undefined,
129+
env: 'qa' as Environment,
130+
};
131+
132+
it('validates successfully when limits are not reached', async () => {
133+
mockedGetAccountId.mockReturnValue(mockAccount.portalId);
134+
mockedGetSandboxUsageLimits.mockResolvedValue({
135+
data: {
136+
usage: { DEVELOPER: { available: 1, limit: 3 } },
137+
},
138+
});
139+
140+
await expect(
141+
validateSandboxUsageLimits(
142+
mockAccount,
143+
HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX,
144+
'qa'
145+
)
146+
).resolves.not.toThrow();
147+
});
148+
149+
it('throws error when development sandbox limit is reached', async () => {
150+
mockedGetAccountId.mockReturnValue(mockAccount.portalId);
151+
mockedGetConfigAccounts.mockReturnValue([]);
152+
mockedGetSandboxUsageLimits.mockResolvedValue({
153+
data: {
154+
usage: { DEVELOPER: { available: 0, limit: 1 } },
155+
},
156+
});
157+
158+
await expect(
159+
validateSandboxUsageLimits(
160+
mockAccount,
161+
HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX,
162+
'qa'
163+
)
164+
).rejects.toThrow(/reached the limit of 1 development sandbox/);
165+
});
166+
});
167+
168+
describe('handleSandboxCreateError()', () => {
169+
const mockEnv = 'qa' as Environment;
170+
const mockName = 'Test Sandbox';
171+
const mockAccountId = 123;
172+
173+
it('handles missing scope error', () => {
174+
const error = new HubSpotHttpError('missing scopes error', {
175+
cause: {
176+
isAxiosError: true,
177+
response: {
178+
status: 403,
179+
data: {
180+
message: 'Missing scopes error',
181+
category: 'MISSING_SCOPES',
182+
},
183+
},
184+
},
185+
});
186+
187+
expect(() =>
188+
handleSandboxCreateError(error, mockEnv, mockName, mockAccountId)
189+
).toThrow(error);
190+
expect(mockedLogger.error).toHaveBeenCalledWith(
191+
expect.stringMatching(
192+
/The personal access key you provided doesn't include sandbox permissions/
193+
)
194+
);
195+
expect(mockedLogger.info).toHaveBeenCalledWith(
196+
expect.stringMatching(/To update CLI permissions for/)
197+
);
198+
});
199+
200+
it('handles user access not allowed error', () => {
201+
const error = new HubSpotHttpError('user access not allowed error', {
202+
cause: {
203+
isAxiosError: true,
204+
response: {
205+
status: 403,
206+
data: {
207+
category: 'BANNED',
208+
subCategory: 'SandboxErrors.USER_ACCESS_NOT_ALLOWED',
209+
},
210+
},
211+
},
212+
});
213+
214+
expect(() =>
215+
handleSandboxCreateError(error, mockEnv, mockName, mockAccountId)
216+
).toThrow(error);
217+
expect(mockedLogger.error).toHaveBeenCalledWith(
218+
expect.stringMatching(
219+
/your permission set doesn't allow you to create the sandbox/
220+
)
221+
);
222+
});
223+
224+
it('handles 403 gating error', () => {
225+
const error = new HubSpotHttpError('403 gating error', {
226+
cause: {
227+
isAxiosError: true,
228+
response: {
229+
status: 403,
230+
data: {
231+
category: 'BANNED',
232+
subCategory:
233+
'SandboxErrors.DEVELOPMENT_SANDBOX_ACCESS_NOT_ALLOWED',
234+
},
235+
},
236+
},
237+
});
238+
239+
expect(() =>
240+
handleSandboxCreateError(error, mockEnv, mockName, mockAccountId)
241+
).toThrow(error);
242+
expect(mockedLogger.error).toHaveBeenCalledWith(
243+
expect.stringMatching(/does not have access to development sandboxes/)
244+
);
245+
});
246+
});
247+
});

lib/sandboxes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export function getSandboxTypeAsString(accountType?: AccountType): string {
4242
return 'standard';
4343
}
4444

45-
function getHasSandboxesByType(
45+
export function getHasSandboxesByType(
4646
parentAccountConfig: CLIAccount,
4747
type: AccountType
4848
): boolean {

0 commit comments

Comments
 (0)