fix(server): key connected-account lookup on handle and provider (#22964)

Connect flows (Google, Microsoft, IMAP/SMTP/CalDAV) looked up an
existing connectedAccount by `handle` alone, so connecting a second
account with the same handle but a different provider overwrote the
first instead of inserting a new row (e.g. IMAP inbox clobbering a
calendar-only Google account).

Fix: add the `provider` discriminator to the lookup. Same
provider+handle still updates; a different provider gets its own row.
Integration test covers the Google-then-IMAP case.
This commit is contained in:
neo773
2026-07-17 00:28:23 +05:30
committed by GitHub
parent 6a1de47a17
commit 52b7aebddf
4 changed files with 41 additions and 1 deletions
@@ -154,6 +154,7 @@ export class GoogleAPIsService {
handle,
userWorkspaceId,
workspaceId,
provider: ConnectedAccountProvider.GOOGLE,
},
});
@@ -120,6 +120,7 @@ export class MicrosoftAPIsService {
handle,
userWorkspaceId: userWorkspaceId,
workspaceId,
provider: ConnectedAccountProvider.MICROSOFT,
},
});
@@ -658,6 +658,7 @@ describe('ImapSmtpCalDavAPIService', () => {
handle: 'test@example.com',
userWorkspaceId: 'user-workspace-id',
workspaceId: 'workspace-id',
provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV,
},
});
@@ -674,6 +675,38 @@ describe('ImapSmtpCalDavAPIService', () => {
});
});
it('should create a new row instead of overriding an account with the same handle under a different provider', async () => {
mockConnectedAccountRepository.findOne.mockResolvedValue(null);
mockMessageChannelRepository.findOne.mockResolvedValue(null);
mockCalendarChannelRepository.findOne.mockResolvedValue(null);
mockWorkspaceMemberRepository.findOne.mockResolvedValue({
id: 'workspace-member-id',
userId: 'user-id',
});
mockUserWorkspaceRepository.findOne.mockResolvedValue({
id: 'user-workspace-id',
userId: 'user-id',
});
await service.upsertConnectedAccount(baseInput);
expect(mockConnectedAccountRepository.findOne).toHaveBeenCalledWith({
where: {
handle: 'test@example.com',
userWorkspaceId: 'user-workspace-id',
workspaceId: 'workspace-id',
provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV,
},
});
expect(mockTransactionManagerSave).toHaveBeenCalledWith(
expect.objectContaining({
id: 'mocked-uuid',
provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV,
}),
);
});
it('should not create channels when neither IMAP nor CALDAV is configured', async () => {
const smtpOnlyInput = {
...baseInput,
@@ -86,7 +86,12 @@ export class ImapSmtpCalDavAPIService {
const existingAccount =
input.existingAccount ??
(await this.connectedAccountRepository.findOne({
where: { handle, userWorkspaceId, workspaceId },
where: {
handle,
userWorkspaceId,
workspaceId,
provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV,
},
}));
const newOrExistingAccountId = existingAccount?.id ?? v4();