Messaging archived account fix (#23553)
Fix case where workspace admin wants to reconnect inherited connected account Case: - workspace admin inherits team accounts from other workspace member who left the workspace - admin wants to reconnect inherited channels but it's not possible as there's no path to make archived connected account active Expected outcome: admin, who has credentials to archived connected accounts, can reconnect said accounts <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23553?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. --> --------- Co-authored-by: neo773 <huzef@twenty.com> Co-authored-by: neo773 <62795688+neo773@users.noreply.github.com>
This commit is contained in:
+66
-1
@@ -47,8 +47,13 @@ describe('GoogleAPIsService', () => {
|
||||
findOne: jest.fn(),
|
||||
};
|
||||
|
||||
const mockTransactionEntityRepository = {
|
||||
save: jest.fn(),
|
||||
update: jest.fn(),
|
||||
};
|
||||
|
||||
const mockTransactionManager = {
|
||||
getRepository: jest.fn().mockReturnValue({ save: jest.fn() }),
|
||||
getRepository: jest.fn().mockReturnValue(mockTransactionEntityRepository),
|
||||
};
|
||||
|
||||
const mockMessageChannelRepository = {
|
||||
@@ -298,5 +303,65 @@ describe('GoogleAPIsService', () => {
|
||||
createMessageChannelService.createMessageChannel,
|
||||
).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should re-enable sync on existing channels when reconnecting an archived account', async () => {
|
||||
mockTwentyConfigService.get.mockImplementation((key) => {
|
||||
if (key === 'CALENDAR_PROVIDER_GOOGLE_ENABLED') return true;
|
||||
if (key === 'MESSAGING_PROVIDER_GMAIL_ENABLED') return true;
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
const archivedConnectedAccount = {
|
||||
id: 'archived-account-id',
|
||||
handle: 'test@example.com',
|
||||
userWorkspaceId: 'user-workspace-id',
|
||||
provider: ConnectedAccountProvider.GOOGLE,
|
||||
archivedAt: new Date('2026-01-01'),
|
||||
} as ConnectedAccountEntity;
|
||||
|
||||
mockConnectedAccountRepository.findOne.mockResolvedValue(
|
||||
archivedConnectedAccount,
|
||||
);
|
||||
|
||||
mockWorkspaceMemberRepository.findOne.mockResolvedValue({
|
||||
id: 'workspace-member-id',
|
||||
userId: 'user-id',
|
||||
});
|
||||
|
||||
mockMessageChannelRepository.find.mockResolvedValue([
|
||||
{
|
||||
id: 'message-channel-id',
|
||||
connectedAccountId: 'archived-account-id',
|
||||
},
|
||||
]);
|
||||
|
||||
mockCalendarChannelRepository.find.mockResolvedValue([
|
||||
{
|
||||
id: 'calendar-channel-id',
|
||||
connectedAccountId: 'archived-account-id',
|
||||
},
|
||||
]);
|
||||
|
||||
await service.refreshGoogleRefreshToken({
|
||||
handle: 'test@example.com',
|
||||
userId: 'user-id',
|
||||
workspaceMemberId: 'workspace-member-id',
|
||||
workspaceId: 'workspace-id',
|
||||
accessToken: 'new-access-token' as PlaintextString,
|
||||
refreshToken: 'new-refresh-token' as PlaintextString,
|
||||
calendarVisibility: CalendarChannelVisibility.SHARE_EVERYTHING,
|
||||
messageVisibility: MessageChannelVisibility.SHARE_EVERYTHING,
|
||||
});
|
||||
|
||||
expect(mockTransactionEntityRepository.update).toHaveBeenCalledWith(
|
||||
{
|
||||
connectedAccountId: 'archived-account-id',
|
||||
workspaceId: 'workspace-id',
|
||||
},
|
||||
{ isSyncEnabled: true },
|
||||
);
|
||||
expect(mockTransactionEntityRepository.update).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -160,6 +160,7 @@ export class GoogleAPIsService {
|
||||
|
||||
const existingAccountId = connectedAccount?.id;
|
||||
const newOrExistingConnectedAccountId = existingAccountId ?? v4();
|
||||
const wasArchived = isDefined(connectedAccount?.archivedAt);
|
||||
|
||||
const existingMessageChannels =
|
||||
await this.messageChannelRepository.find({
|
||||
@@ -239,6 +240,40 @@ export class GoogleAPIsService {
|
||||
transactionManager,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
wasArchived &&
|
||||
isMessagingEnabled &&
|
||||
isMessagingAvailable &&
|
||||
existingMessageChannels.length > 0
|
||||
) {
|
||||
await transactionManager
|
||||
.getRepository(MessageChannelEntity)
|
||||
.update(
|
||||
{
|
||||
connectedAccountId: newOrExistingConnectedAccountId,
|
||||
workspaceId,
|
||||
},
|
||||
{ isSyncEnabled: true },
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
wasArchived &&
|
||||
isCalendarEnabled &&
|
||||
isCalendarAvailable &&
|
||||
existingCalendarChannels.length > 0
|
||||
) {
|
||||
await transactionManager
|
||||
.getRepository(CalendarChannelEntity)
|
||||
.update(
|
||||
{
|
||||
connectedAccountId: newOrExistingConnectedAccountId,
|
||||
workspaceId,
|
||||
},
|
||||
{ isSyncEnabled: true },
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
+66
-1
@@ -44,8 +44,13 @@ describe('MicrosoftAPIsService', () => {
|
||||
findOne: jest.fn(),
|
||||
};
|
||||
|
||||
const mockTransactionEntityRepository = {
|
||||
save: jest.fn(),
|
||||
update: jest.fn(),
|
||||
};
|
||||
|
||||
const mockTransactionManager = {
|
||||
getRepository: jest.fn().mockReturnValue({ save: jest.fn() }),
|
||||
getRepository: jest.fn().mockReturnValue(mockTransactionEntityRepository),
|
||||
};
|
||||
|
||||
const mockMessageChannelRepository = {
|
||||
@@ -277,5 +282,65 @@ describe('MicrosoftAPIsService', () => {
|
||||
createMessageChannelService.createMessageChannel,
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should re-enable sync on existing channels when reconnecting an archived account', async () => {
|
||||
mockTwentyConfigService.get.mockImplementation((key) => {
|
||||
if (key === 'CALENDAR_PROVIDER_MICROSOFT_ENABLED') return true;
|
||||
if (key === 'MESSAGING_PROVIDER_MICROSOFT_ENABLED') return true;
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
const archivedConnectedAccount = {
|
||||
id: 'archived-account-id',
|
||||
handle: 'test@example.com',
|
||||
userWorkspaceId: 'user-workspace-id',
|
||||
provider: ConnectedAccountProvider.MICROSOFT,
|
||||
archivedAt: new Date('2026-01-01'),
|
||||
} as ConnectedAccountEntity;
|
||||
|
||||
mockConnectedAccountRepository.findOne.mockResolvedValue(
|
||||
archivedConnectedAccount,
|
||||
);
|
||||
|
||||
mockWorkspaceMemberRepository.findOne.mockResolvedValue({
|
||||
id: 'workspace-member-id',
|
||||
userId: 'user-id',
|
||||
});
|
||||
|
||||
mockMessageChannelRepository.find.mockResolvedValue([
|
||||
{
|
||||
id: 'message-channel-id',
|
||||
connectedAccountId: 'archived-account-id',
|
||||
},
|
||||
]);
|
||||
|
||||
mockCalendarChannelRepository.find.mockResolvedValue([
|
||||
{
|
||||
id: 'calendar-channel-id',
|
||||
connectedAccountId: 'archived-account-id',
|
||||
},
|
||||
]);
|
||||
|
||||
await service.refreshMicrosoftRefreshToken({
|
||||
handle: 'test@example.com',
|
||||
userId: 'user-id',
|
||||
workspaceMemberId: 'workspace-member-id',
|
||||
workspaceId: 'workspace-id',
|
||||
accessToken: 'new-access-token' as PlaintextString,
|
||||
refreshToken: 'new-refresh-token' as PlaintextString,
|
||||
calendarVisibility: CalendarChannelVisibility.SHARE_EVERYTHING,
|
||||
messageVisibility: MessageChannelVisibility.SHARE_EVERYTHING,
|
||||
});
|
||||
|
||||
expect(mockTransactionEntityRepository.update).toHaveBeenCalledWith(
|
||||
{
|
||||
connectedAccountId: 'archived-account-id',
|
||||
workspaceId: 'workspace-id',
|
||||
},
|
||||
{ isSyncEnabled: true },
|
||||
);
|
||||
expect(mockTransactionEntityRepository.update).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+25
@@ -126,6 +126,7 @@ export class MicrosoftAPIsService {
|
||||
|
||||
const existingAccountId = connectedAccount?.id;
|
||||
const newOrExistingConnectedAccountId = existingAccountId ?? v4();
|
||||
const wasArchived = isDefined(connectedAccount?.archivedAt);
|
||||
|
||||
const existingMessageChannels =
|
||||
await this.messageChannelRepository.find({
|
||||
@@ -217,6 +218,30 @@ export class MicrosoftAPIsService {
|
||||
transactionManager,
|
||||
});
|
||||
}
|
||||
|
||||
if (wasArchived && existingMessageChannels.length > 0) {
|
||||
await transactionManager
|
||||
.getRepository(MessageChannelEntity)
|
||||
.update(
|
||||
{
|
||||
connectedAccountId: newOrExistingConnectedAccountId,
|
||||
workspaceId,
|
||||
},
|
||||
{ isSyncEnabled: true },
|
||||
);
|
||||
}
|
||||
|
||||
if (wasArchived && existingCalendarChannels.length > 0) {
|
||||
await transactionManager
|
||||
.getRepository(CalendarChannelEntity)
|
||||
.update(
|
||||
{
|
||||
connectedAccountId: newOrExistingConnectedAccountId,
|
||||
workspaceId,
|
||||
},
|
||||
{ isSyncEnabled: true },
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
+1
@@ -60,6 +60,7 @@ export class UpdateConnectedAccountOnReconnectService {
|
||||
refreshToken: encryptedRefreshToken,
|
||||
scopes,
|
||||
authFailedAt: null,
|
||||
archivedAt: null,
|
||||
},
|
||||
);
|
||||
}, authContext);
|
||||
|
||||
Reference in New Issue
Block a user