fix(editor): Show switch-to-static warning after connecting a private credential (#31712)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Guillaume Jacquart 2026-06-04 15:56:52 +02:00 committed by GitHub
parent f723f54879
commit f1d87fddb2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 73 additions and 2 deletions

View File

@ -13,6 +13,7 @@ import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import type { ICredentialsResponse } from '../../credentials.types';
import { within, waitFor } from '@testing-library/vue';
import userEvent from '@testing-library/user-event';
import type { ICredentialType, INode, INodeTypeDescription } from 'n8n-workflow';
vi.mock('vue-router', async () => ({
@ -32,6 +33,17 @@ vi.mock('@/app/composables/useToast', () => ({
}),
}));
const { confirmMock } = vi.hoisted(() => ({ confirmMock: vi.fn() }));
vi.mock('@/app/composables/useMessage', () => ({
useMessage: () => ({ confirm: confirmMock }),
}));
vi.mock('@/features/resolvers/composables/useDynamicCredentials', async () => {
const { ref } = await vi.importActual<typeof import('vue')>('vue');
return { useDynamicCredentials: () => ({ isEnabled: ref(true) }) };
});
const oAuth2Api: ICredentialType = {
name: 'oAuth2Api',
displayName: 'OAuth2 API',
@ -946,5 +958,60 @@ describe('CredentialEdit', () => {
await retry(() => expect(credentialsStore.getCredentialData).toHaveBeenCalled());
await retry(() => expect(queryByTestId('oauth-not-connected-banner')).not.toBeVisible());
});
describe('switching a connected private credential to static', () => {
test('shows the confirmation modal when the current user just connected, even if the server count is stale', async () => {
confirmMock.mockResolvedValue('confirm');
const pinia = createPiniaForBannerTest();
// connectedByMe reflects the in-session connection; connectedUserCount is the
// stale server value (0) that does not yet include the current user.
const credentialsStore = setupOAuthCredential({
isResolvable: true,
connectedByMe: true,
connectedUserCount: 0,
});
const { getByRole } = renderComponent({
props: {
activeId: 'cred-banner',
modalName: CREDENTIAL_EDIT_MODAL_KEY,
mode: 'edit',
},
pinia,
});
await retry(() => expect(credentialsStore.getCredentialData).toHaveBeenCalled());
await userEvent.click(getByRole('switch'));
await retry(() => expect(confirmMock).toHaveBeenCalled());
expect(confirmMock.mock.calls[0][0]).toContain('1 user(s)');
});
test('does not show the confirmation modal when no users are connected', async () => {
confirmMock.mockResolvedValue('confirm');
const pinia = createPiniaForBannerTest();
const credentialsStore = setupOAuthCredential({
isResolvable: true,
connectedByMe: false,
connectedUserCount: 0,
});
const { getByRole } = renderComponent({
props: {
activeId: 'cred-banner',
modalName: CREDENTIAL_EDIT_MODAL_KEY,
mode: 'edit',
},
pinia,
});
await retry(() => expect(credentialsStore.getCredentialData).toHaveBeenCalled());
await userEvent.click(getByRole('switch'));
expect(confirmMock).not.toHaveBeenCalled();
});
});
});
});

View File

@ -785,8 +785,12 @@ async function onResolvableChange(value: boolean) {
return;
}
} else if (isTogglingToStatic) {
// Private Static: warn only when there are connected users to disconnect
const connectedUserCount = currentCredential.value?.connectedUserCount ?? 0;
// Private Static: warn only when there are connected users to disconnect.
// `connectedUserCount` reflects the server state at modal-open and isn't
// refreshed when the current user connects within the same session, so fold
// in `connectedByMe` to make sure the warning still appears in that case.
const serverConnectedCount = currentCredential.value?.connectedUserCount ?? 0;
const connectedUserCount = Math.max(serverConnectedCount, connectedByMe.value ? 1 : 0);
if (connectedUserCount > 0) {
const confirmAction = await confirmModal('switchToStatic', {
count: String(connectedUserCount),