Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: unit tests for polling #1353

Merged
merged 3 commits into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added commands/account/hello.ts
brandenrodgers marked this conversation as resolved.
Show resolved Hide resolved
Empty file.
4 changes: 3 additions & 1 deletion commands/function/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ exports.handler = async options => {
derivedAccountId,
functionPath
);
const successResp = await poll(getBuildStatus, derivedAccountId, buildId);
const successResp = await poll(() =>
getBuildStatus(derivedAccountId, buildId)
);
const buildTimeSeconds = (successResp.buildTime / 1000).toFixed(2);

SpinniesManager.succeed('loading');
Expand Down
4 changes: 3 additions & 1 deletion commands/project/cloneApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,9 @@ exports.handler = async options => {
const {
data: { exportId },
} = await cloneApp(derivedAccountId, appId);
const { status } = await poll(checkCloneStatus, derivedAccountId, exportId);
const { status } = await poll(() =>
checkCloneStatus(derivedAccountId, exportId)
);
if (status === 'SUCCESS') {
// Ensure correct project folder structure exists
const baseDestPath = path.resolve(getCwd(), projectDest);
Expand Down
4 changes: 3 additions & 1 deletion commands/project/migrateApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,9 @@ exports.handler = async options => {
projectName
);
const { id } = migrateResponse;
const pollResponse = await poll(checkMigrationStatus, derivedAccountId, id);
const pollResponse = await poll(() =>
checkMigrationStatus(derivedAccountId, id)
);
const { status, project } = pollResponse;
if (status === 'SUCCESS') {
const absoluteDestPath = path.resolve(getCwd(), projectDest);
Expand Down
107 changes: 107 additions & 0 deletions lib/__tests__/polling.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { poll, DEFAULT_POLLING_STATES } from '../polling';
import { DEFAULT_POLLING_DELAY } from '../constants';
import { HubSpotPromise } from '@hubspot/local-dev-lib/types/Http';

// Mock response types
type MockResponse = {
status: string;
};

// Helper to create a mock polling callback
const createMockCallback = (responses: MockResponse[]) => {
let callCount = 0;
return jest.fn((): HubSpotPromise<{ status: string }> => {
const response = responses[callCount];
callCount++;
return Promise.resolve({ data: response }) as HubSpotPromise<{
status: string;
}>;
});
};

describe('lib/polling', () => {
beforeEach(() => {
jest.useFakeTimers();
});

afterEach(() => {
jest.useRealTimers();
});

describe('poll()', () => {
it('should resolve when status is SUCCESS', async () => {
const mockCallback = createMockCallback([
{ status: DEFAULT_POLLING_STATES.STARTED },
{ status: DEFAULT_POLLING_STATES.SUCCESS },
]);

const pollPromise = poll(mockCallback);

// Fast-forward through two polling intervals
jest.advanceTimersByTime(DEFAULT_POLLING_DELAY * 2);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor came up with the idea to use advanceTimersByTime, but it's a cool way to test utils that leverage setTimeout. This gives us control over each execution of the setTimeout in poll(). So we can iterate over the mock responses one at a time.


const result = await pollPromise;
expect(result.status).toBe(DEFAULT_POLLING_STATES.SUCCESS);
expect(mockCallback).toHaveBeenCalledTimes(2);
});

it('should reject when status is ERROR', async () => {
const mockCallback = createMockCallback([
{ status: DEFAULT_POLLING_STATES.STARTED },
{ status: DEFAULT_POLLING_STATES.ERROR },
]);

const pollPromise = poll(mockCallback);

jest.advanceTimersByTime(DEFAULT_POLLING_DELAY * 2);

await expect(pollPromise).rejects.toEqual({
status: DEFAULT_POLLING_STATES.ERROR,
});
expect(mockCallback).toHaveBeenCalledTimes(2);
});

it('should reject when status is FAILURE', async () => {
const mockCallback = createMockCallback([
{ status: DEFAULT_POLLING_STATES.STARTED },
{ status: DEFAULT_POLLING_STATES.FAILURE },
]);

const pollPromise = poll(mockCallback);

jest.advanceTimersByTime(DEFAULT_POLLING_DELAY * 2);

await expect(pollPromise).rejects.toEqual({
status: DEFAULT_POLLING_STATES.FAILURE,
});
});

it('should reject when status is REVERTED', async () => {
const mockCallback = createMockCallback([
{ status: DEFAULT_POLLING_STATES.STARTED },
{ status: DEFAULT_POLLING_STATES.REVERTED },
]);

const pollPromise = poll(mockCallback);

jest.advanceTimersByTime(DEFAULT_POLLING_DELAY * 2);

await expect(pollPromise).rejects.toEqual({
status: DEFAULT_POLLING_STATES.REVERTED,
});
});

it('should reject when callback throws an error', async () => {
const mockCallback = jest
.fn()
.mockRejectedValue(new Error('Network error'));

const pollPromise = poll(mockCallback);

jest.advanceTimersByTime(DEFAULT_POLLING_DELAY);

await expect(pollPromise).rejects.toThrow('Network error');
expect(mockCallback).toHaveBeenCalledTimes(1);
});
});
});
9 changes: 1 addition & 8 deletions lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,7 @@ export const CONFIG_FLAGS = {
USE_CUSTOM_OBJECT_HUBFILE: 'useCustomObjectHubfile',
} as const;

export const POLLING_DELAY = 2000;

export const POLLING_STATUS = {
SUCCESS: 'SUCCESS',
ERROR: 'ERROR',
REVERTED: 'REVERTED',
FAILURE: 'FAILURE',
} as const;
export const DEFAULT_POLLING_DELAY = 2000;

export const PROJECT_CONFIG_FILE = 'hsproject.json' as const;

Expand Down
46 changes: 29 additions & 17 deletions lib/polling.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,54 @@
import { HubSpotPromise } from '@hubspot/local-dev-lib/types/Http';
import { ValueOf } from '@hubspot/local-dev-lib/types/Utils';
import { POLLING_DELAY, POLLING_STATUS } from './constants';
import { DEFAULT_POLLING_DELAY } from './constants';

export const DEFAULT_POLLING_STATES = {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved this here because it's only relevant to this file

STARTED: 'STARTED',
SUCCESS: 'SUCCESS',
ERROR: 'ERROR',
REVERTED: 'REVERTED',
FAILURE: 'FAILURE',
} as const;

const DEFAULT_POLLING_STATUS_LOOKUP = {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This lookup will enable this polling func to be used for other apis, even if their status enums are slightly different from the default ones here. The only requirement in the polling util now is that the callback response needs to include a status field.

successStates: [DEFAULT_POLLING_STATES.SUCCESS],
errorStates: [
DEFAULT_POLLING_STATES.ERROR,
DEFAULT_POLLING_STATES.REVERTED,
DEFAULT_POLLING_STATES.FAILURE,
],
};

type GenericPollingResponse = {
status: ValueOf<typeof POLLING_STATUS>;
status: string;
};

type PollingCallback<T extends GenericPollingResponse> = (
accountId: number,
taskId: number | string
) => HubSpotPromise<T>;
type PollingCallback<T extends GenericPollingResponse> =
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the signature of the callback so we could simplify the arguments in poll. This makes the polling function more generic so it can be more easily leveraged in other places.

() => HubSpotPromise<T>;

export function poll<T extends GenericPollingResponse>(
callback: PollingCallback<T>,
accountId: number,
taskId: number | string
statusLookup?: { successStates: string[]; errorStates: string[] }
): Promise<T> {
if (!statusLookup) {
statusLookup = DEFAULT_POLLING_STATUS_LOOKUP;
brandenrodgers marked this conversation as resolved.
Show resolved Hide resolved
}
return new Promise((resolve, reject) => {
const pollInterval = setInterval(async () => {
try {
const { data: pollResp } = await callback(accountId, taskId);
const { data: pollResp } = await callback();
const { status } = pollResp;

if (status === POLLING_STATUS.SUCCESS) {
if (statusLookup.successStates.includes(status)) {
clearInterval(pollInterval);
resolve(pollResp);
} else if (
status === POLLING_STATUS.ERROR ||
status === POLLING_STATUS.REVERTED ||
status === POLLING_STATUS.FAILURE
) {
} else if (statusLookup.errorStates.includes(status)) {
clearInterval(pollInterval);
reject(pollResp);
}
} catch (error) {
clearInterval(pollInterval);
reject(error);
}
}, POLLING_DELAY);
}, DEFAULT_POLLING_DELAY);
});
}
8 changes: 4 additions & 4 deletions lib/projects/buildAndDeploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
import { WarnLogsResponse } from '@hubspot/local-dev-lib/types/Project';

import {
POLLING_DELAY,
DEFAULT_POLLING_DELAY,
PROJECT_BUILD_TEXT,
PROJECT_DEPLOY_TEXT,
PROJECT_TASK_TYPES,
Expand Down Expand Up @@ -366,7 +366,7 @@ function makePollTaskStatusFunc<T extends ProjectTask>({
resolve(taskStatus);
}
}
}, POLLING_DELAY);
}, DEFAULT_POLLING_DELAY);
});
};
}
Expand All @@ -377,7 +377,7 @@ function pollBuildAutodeployStatus(
buildId: number
): Promise<Build> {
return new Promise((resolve, reject) => {
let maxIntervals = (30 * 1000) / POLLING_DELAY; // Num of intervals in ~30s
let maxIntervals = (30 * 1000) / DEFAULT_POLLING_DELAY; // Num of intervals in ~30s

const pollInterval = setInterval(async () => {
let build: Build;
Expand Down Expand Up @@ -407,7 +407,7 @@ function pollBuildAutodeployStatus(
} else {
maxIntervals -= 1;
}
}, POLLING_DELAY);
}, DEFAULT_POLLING_DELAY);
});
}

Expand Down
4 changes: 2 additions & 2 deletions lib/projects/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { HubSpotPromise } from '@hubspot/local-dev-lib/types/Http';

import {
FEEDBACK_INTERVAL,
POLLING_DELAY,
DEFAULT_POLLING_DELAY,
PROJECT_CONFIG_FILE,
HUBSPOT_PROJECT_COMPONENTS_GITHUB_PATH,
PROJECT_COMPONENT_TYPES,
Expand Down Expand Up @@ -242,7 +242,7 @@ async function pollFetchProject(
reject(err);
}
}
}, POLLING_DELAY);
}, DEFAULT_POLLING_DELAY);
});
}

Expand Down
Loading