d99e6db93d
13 integration suites driving the real sync pipeline end to end — OAuth connect via the actual `/auth/google-apis/get-access-token` / `microsoft-apis` callbacks (transient token + mocked provider token exchange), real queue workers, provider APIs mocked at the HTTP layer with msw. **Messaging (8):** Gmail list fetch + import, Gmail folder discovery, Microsoft folder discovery, history-based incremental sync, stale-sync recovery, sync failure lifecycle (429 throttle → exhaustion → relaunch; declined refresh token → insufficient permissions), token refresh, connected-account cleanup cascade. **Calendar (5):** Google events import (full + sync-token incremental), Microsoft events import (delta fetch + import), stale-sync recovery, failure lifecycle, cleanup cascade. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22567?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
302 lines
7.4 KiB
TypeScript
302 lines
7.4 KiB
TypeScript
import gql from 'graphql-tag';
|
|
|
|
import { type MessageFolderImportPolicy } from 'twenty-shared/types';
|
|
|
|
import { type CalendarChannelDTO } from 'src/engine/metadata-modules/calendar-channel/dtos/calendar-channel.dto';
|
|
import { type ConnectedAccountDTO } from 'src/engine/metadata-modules/connected-account/dtos/connected-account.dto';
|
|
import { type MessageChannelDTO } from 'src/engine/metadata-modules/message-channel/dtos/message-channel.dto';
|
|
import { type MessageFolderDTO } from 'src/engine/metadata-modules/message-folder/dtos/message-folder.dto';
|
|
|
|
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
|
|
import { waitForAllJobsToFinish } from 'test/integration/utils/wait-for-all-jobs-to-finish.util';
|
|
|
|
type MetadataAPIResponse = {
|
|
body: { data: Record<string, unknown>; errors?: { message: string }[] };
|
|
};
|
|
|
|
export const getDataOrThrow = (response: MetadataAPIResponse) => {
|
|
if (response.body.errors?.length) {
|
|
throw new Error(
|
|
`Metadata API request failed: ${response.body.errors
|
|
.map((error) => error.message)
|
|
.join('; ')}`,
|
|
);
|
|
}
|
|
|
|
return response.body.data;
|
|
};
|
|
|
|
export type MessageFolderDto = Pick<
|
|
MessageFolderDTO,
|
|
'id' | 'name' | 'isSynced'
|
|
>;
|
|
|
|
export type MessageChannelDto = Pick<
|
|
MessageChannelDTO,
|
|
| 'id'
|
|
| 'handle'
|
|
| 'connectedAccountId'
|
|
| 'syncStatus'
|
|
| 'syncStage'
|
|
| 'syncStageStartedAt'
|
|
| 'throttleFailureCount'
|
|
| 'throttleRetryAfter'
|
|
>;
|
|
|
|
export type CalendarChannelDto = Pick<
|
|
CalendarChannelDTO,
|
|
| 'id'
|
|
| 'handle'
|
|
| 'connectedAccountId'
|
|
| 'syncStatus'
|
|
| 'syncStage'
|
|
| 'syncStageStartedAt'
|
|
| 'throttleFailureCount'
|
|
>;
|
|
|
|
export type ConnectedAccountDto = Pick<
|
|
ConnectedAccountDTO,
|
|
'id' | 'handle' | 'provider' | 'lastCredentialsRefreshedAt' | 'authFailedAt'
|
|
>;
|
|
|
|
type MessageChannelUpdate = {
|
|
messageFolderImportPolicy?: MessageFolderImportPolicy;
|
|
isSyncEnabled?: boolean;
|
|
isContactAutoCreationEnabled?: boolean;
|
|
};
|
|
|
|
type CalendarChannelUpdate = {
|
|
isSyncEnabled?: boolean;
|
|
isContactAutoCreationEnabled?: boolean;
|
|
};
|
|
|
|
const MESSAGE_CHANNEL_FIELDS = gql`
|
|
fragment TestMessageChannelFields on MessageChannel {
|
|
id
|
|
handle
|
|
connectedAccountId
|
|
syncStatus
|
|
syncStage
|
|
syncStageStartedAt
|
|
throttleFailureCount
|
|
throttleRetryAfter
|
|
}
|
|
`;
|
|
|
|
export const queryMessageChannels = async (): Promise<MessageChannelDto[]> => {
|
|
const response = await makeMetadataAPIRequest({
|
|
query: gql`
|
|
query MessageChannelsForTest {
|
|
myMessageChannels {
|
|
...TestMessageChannelFields
|
|
}
|
|
}
|
|
${MESSAGE_CHANNEL_FIELDS}
|
|
`,
|
|
});
|
|
|
|
return getDataOrThrow(response).myMessageChannels as MessageChannelDto[];
|
|
};
|
|
|
|
export const queryMessageChannel = async ({
|
|
connectedAccountId,
|
|
channelId,
|
|
}: {
|
|
connectedAccountId: string;
|
|
channelId: string;
|
|
}): Promise<MessageChannelDto> => {
|
|
const response = await makeMetadataAPIRequest({
|
|
query: gql`
|
|
query MessageChannelForTest($connectedAccountId: UUID) {
|
|
myMessageChannels(connectedAccountId: $connectedAccountId) {
|
|
...TestMessageChannelFields
|
|
}
|
|
}
|
|
${MESSAGE_CHANNEL_FIELDS}
|
|
`,
|
|
variables: { connectedAccountId },
|
|
});
|
|
|
|
const channel = (
|
|
getDataOrThrow(response).myMessageChannels as MessageChannelDto[]
|
|
).find((candidate) => candidate.id === channelId);
|
|
|
|
if (!channel) {
|
|
throw new Error(`Message channel ${channelId} not found`);
|
|
}
|
|
|
|
return channel;
|
|
};
|
|
|
|
export const queryMessageFolders = async (
|
|
messageChannelId: string,
|
|
): Promise<MessageFolderDto[]> => {
|
|
const response = await makeMetadataAPIRequest({
|
|
query: gql`
|
|
query MessageFoldersForTest($messageChannelId: UUID) {
|
|
myMessageFolders(messageChannelId: $messageChannelId) {
|
|
id
|
|
name
|
|
isSynced
|
|
}
|
|
}
|
|
`,
|
|
variables: { messageChannelId },
|
|
});
|
|
|
|
return getDataOrThrow(response).myMessageFolders as MessageFolderDto[];
|
|
};
|
|
|
|
export const queryCalendarChannels = async (
|
|
connectedAccountId: string,
|
|
): Promise<CalendarChannelDto[]> => {
|
|
const response = await makeMetadataAPIRequest({
|
|
query: gql`
|
|
query CalendarChannelsForTest($connectedAccountId: UUID) {
|
|
myCalendarChannels(connectedAccountId: $connectedAccountId) {
|
|
id
|
|
handle
|
|
connectedAccountId
|
|
syncStatus
|
|
syncStage
|
|
syncStageStartedAt
|
|
throttleFailureCount
|
|
}
|
|
}
|
|
`,
|
|
variables: { connectedAccountId },
|
|
});
|
|
|
|
return getDataOrThrow(response).myCalendarChannels as CalendarChannelDto[];
|
|
};
|
|
|
|
export const queryCalendarChannel = async ({
|
|
connectedAccountId,
|
|
calendarChannelId,
|
|
}: {
|
|
connectedAccountId: string;
|
|
calendarChannelId: string;
|
|
}): Promise<CalendarChannelDto> => {
|
|
const channel = (await queryCalendarChannels(connectedAccountId)).find(
|
|
(candidate) => candidate.id === calendarChannelId,
|
|
);
|
|
|
|
if (!channel) {
|
|
throw new Error(`Calendar channel ${calendarChannelId} not found`);
|
|
}
|
|
|
|
return channel;
|
|
};
|
|
|
|
export const queryConnectedAccount = async (
|
|
connectedAccountId: string,
|
|
): Promise<ConnectedAccountDto> => {
|
|
const response = await makeMetadataAPIRequest({
|
|
query: gql`
|
|
query ConnectedAccountsForTest {
|
|
myConnectedAccounts {
|
|
id
|
|
handle
|
|
provider
|
|
lastCredentialsRefreshedAt
|
|
authFailedAt
|
|
}
|
|
}
|
|
`,
|
|
});
|
|
|
|
const account = (
|
|
getDataOrThrow(response).myConnectedAccounts as ConnectedAccountDto[]
|
|
).find((candidate) => candidate.id === connectedAccountId);
|
|
|
|
if (!account) {
|
|
throw new Error(`Connected account ${connectedAccountId} not found`);
|
|
}
|
|
|
|
return account;
|
|
};
|
|
|
|
export const updateMessageChannel = async (
|
|
messageChannelId: string,
|
|
update: MessageChannelUpdate,
|
|
): Promise<void> => {
|
|
const response = await makeMetadataAPIRequest({
|
|
query: gql`
|
|
mutation UpdateMessageChannelForTest($input: UpdateMessageChannelInput!) {
|
|
updateMessageChannel(input: $input) {
|
|
id
|
|
}
|
|
}
|
|
`,
|
|
variables: {
|
|
input: {
|
|
id: messageChannelId,
|
|
update,
|
|
},
|
|
},
|
|
});
|
|
|
|
getDataOrThrow(response);
|
|
};
|
|
|
|
export const updateCalendarChannel = async (
|
|
calendarChannelId: string,
|
|
update: CalendarChannelUpdate,
|
|
): Promise<void> => {
|
|
const response = await makeMetadataAPIRequest({
|
|
query: gql`
|
|
mutation UpdateCalendarChannelForTest(
|
|
$input: UpdateCalendarChannelInput!
|
|
) {
|
|
updateCalendarChannel(input: $input) {
|
|
id
|
|
}
|
|
}
|
|
`,
|
|
variables: {
|
|
input: {
|
|
id: calendarChannelId,
|
|
update,
|
|
},
|
|
},
|
|
});
|
|
|
|
getDataOrThrow(response);
|
|
};
|
|
|
|
export const startChannelSync = async (
|
|
connectedAccountId: string,
|
|
): Promise<void> => {
|
|
const response = await makeMetadataAPIRequest({
|
|
query: gql`
|
|
mutation StartChannelSyncForTest($connectedAccountId: UUID!) {
|
|
startChannelSync(connectedAccountId: $connectedAccountId) {
|
|
success
|
|
}
|
|
}
|
|
`,
|
|
variables: { connectedAccountId },
|
|
});
|
|
|
|
getDataOrThrow(response);
|
|
};
|
|
|
|
export const deleteConnectedAccount = async (
|
|
connectedAccountId: string,
|
|
): Promise<void> => {
|
|
const response = await makeMetadataAPIRequest({
|
|
query: gql`
|
|
mutation DeleteConnectedAccountForTest($id: UUID!) {
|
|
deleteConnectedAccount(id: $id) {
|
|
id
|
|
}
|
|
}
|
|
`,
|
|
variables: { id: connectedAccountId },
|
|
});
|
|
|
|
getDataOrThrow(response);
|
|
|
|
await waitForAllJobsToFinish();
|
|
};
|