mirror of
https://github.com/n8n-io/n8n.git
synced 2025-02-21 02:56:40 -08:00
fix(editor): Load only personal credentials when setting up a template (#12826)
This commit is contained in:
parent
b17cbec3af
commit
814e2a8924
|
@ -0,0 +1,276 @@
|
|||
import type { ICredentialMap, ICredentialTypeMap } from '@/Interface';
|
||||
|
||||
export const TEST_CREDENTIALS: ICredentialMap = {
|
||||
// OpenAI credential in personal
|
||||
1: {
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
id: '1',
|
||||
name: 'OpenAi account',
|
||||
data: 'test123',
|
||||
type: 'openAiApi',
|
||||
isManaged: false,
|
||||
homeProject: {
|
||||
id: '1',
|
||||
type: 'personal',
|
||||
name: 'Kobi Dog <kobi@n8n.io>',
|
||||
icon: null,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
sharedWithProjects: [
|
||||
{
|
||||
id: '2',
|
||||
type: 'team',
|
||||
name: 'Test Project',
|
||||
icon: { type: 'icon', value: 'exchange-alt' },
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
scopes: [
|
||||
'credential:create',
|
||||
'credential:delete',
|
||||
'credential:list',
|
||||
'credential:move',
|
||||
'credential:read',
|
||||
'credential:share',
|
||||
'credential:update',
|
||||
],
|
||||
},
|
||||
// Supabase credential in another project
|
||||
2: {
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
id: '2',
|
||||
name: 'Supabase account',
|
||||
data: 'test123',
|
||||
type: 'supabaseApi',
|
||||
isManaged: false,
|
||||
homeProject: {
|
||||
id: '2',
|
||||
type: 'team',
|
||||
name: 'Test Project',
|
||||
icon: { type: 'icon', value: 'exchange-alt' },
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
sharedWithProjects: [],
|
||||
scopes: [
|
||||
'credential:create',
|
||||
'credential:delete',
|
||||
'credential:list',
|
||||
'credential:move',
|
||||
'credential:read',
|
||||
'credential:share',
|
||||
'credential:update',
|
||||
],
|
||||
},
|
||||
// Slack account in personal
|
||||
3: {
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
id: '3',
|
||||
name: 'Slack account',
|
||||
data: 'test123',
|
||||
type: 'slackOAuth2Api',
|
||||
isManaged: false,
|
||||
homeProject: {
|
||||
id: '1',
|
||||
type: 'personal',
|
||||
name: 'Kobi Dog <kobi@n8n.io>',
|
||||
icon: null,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
sharedWithProjects: [],
|
||||
scopes: [
|
||||
'credential:create',
|
||||
'credential:delete',
|
||||
'credential:list',
|
||||
'credential:move',
|
||||
'credential:read',
|
||||
'credential:share',
|
||||
'credential:update',
|
||||
],
|
||||
},
|
||||
// OpenAI credential in another project
|
||||
4: {
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
id: '4',
|
||||
name: '[PROJECT] OpenAI Account',
|
||||
data: 'test123',
|
||||
type: 'openAiApi',
|
||||
isManaged: false,
|
||||
homeProject: {
|
||||
id: '2',
|
||||
type: 'team',
|
||||
name: 'Test Project',
|
||||
icon: { type: 'icon', value: 'exchange-alt' },
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
sharedWithProjects: [],
|
||||
scopes: [
|
||||
'credential:create',
|
||||
'credential:delete',
|
||||
'credential:list',
|
||||
'credential:move',
|
||||
'credential:read',
|
||||
'credential:share',
|
||||
'credential:update',
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const TEST_CREDENTIAL_TYPES: ICredentialTypeMap = {
|
||||
openAiApi: {
|
||||
name: 'openAiApi',
|
||||
displayName: 'OpenAi',
|
||||
documentationUrl: 'openAi',
|
||||
properties: [
|
||||
{
|
||||
displayName: 'API Key',
|
||||
name: 'apiKey',
|
||||
type: 'string',
|
||||
typeOptions: {
|
||||
password: true,
|
||||
},
|
||||
required: true,
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Base URL',
|
||||
name: 'url',
|
||||
type: 'string',
|
||||
default: 'https://api.openai.com/v1',
|
||||
description: 'Override the base URL for the API',
|
||||
},
|
||||
],
|
||||
authenticate: {
|
||||
type: 'generic',
|
||||
properties: {
|
||||
headers: {
|
||||
Authorization: '=Bearer {{$credentials.apiKey}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
test: {
|
||||
request: {
|
||||
baseURL: '={{$credentials?.url}}',
|
||||
url: '/models',
|
||||
},
|
||||
},
|
||||
iconUrl: {
|
||||
light: 'icons/n8n-nodes-base/dist/nodes/OpenAi/openai.svg',
|
||||
dark: 'icons/n8n-nodes-base/dist/nodes/OpenAi/openai.dark.svg',
|
||||
},
|
||||
supportedNodes: [
|
||||
'n8n-nodes-base.openAi',
|
||||
'@n8n/n8n-nodes-langchain.embeddingsOpenAi',
|
||||
'@n8n/n8n-nodes-langchain.lmChatOpenAi',
|
||||
'@n8n/n8n-nodes-langchain.lmOpenAi',
|
||||
],
|
||||
},
|
||||
supabaseApi: {
|
||||
name: 'supabaseApi',
|
||||
displayName: 'Supabase API',
|
||||
documentationUrl: 'supabase',
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Host',
|
||||
name: 'host',
|
||||
type: 'string',
|
||||
placeholder: 'https://your_account.supabase.co',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Service Role Secret',
|
||||
name: 'serviceRole',
|
||||
type: 'string',
|
||||
default: '',
|
||||
typeOptions: {
|
||||
password: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
authenticate: {
|
||||
type: 'generic',
|
||||
properties: {
|
||||
headers: {
|
||||
apikey: '={{$credentials.serviceRole}}',
|
||||
Authorization: '=Bearer {{$credentials.serviceRole}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
test: {
|
||||
request: {
|
||||
baseURL: '={{$credentials.host}}/rest/v1',
|
||||
headers: {
|
||||
Prefer: 'return=representation',
|
||||
},
|
||||
url: '/',
|
||||
},
|
||||
},
|
||||
iconUrl: 'icons/n8n-nodes-base/dist/nodes/Supabase/supabase.svg',
|
||||
supportedNodes: ['n8n-nodes-base.supabase'],
|
||||
},
|
||||
slackOAuth2Api: {
|
||||
name: 'slackOAuth2Api',
|
||||
extends: ['oAuth2Api'],
|
||||
displayName: 'Slack OAuth2 API',
|
||||
documentationUrl: 'slack',
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Grant Type',
|
||||
name: 'grantType',
|
||||
type: 'hidden',
|
||||
default: 'authorizationCode',
|
||||
},
|
||||
{
|
||||
displayName: 'Authorization URL',
|
||||
name: 'authUrl',
|
||||
type: 'hidden',
|
||||
default: 'https://slack.com/oauth/v2/authorize',
|
||||
},
|
||||
{
|
||||
displayName: 'Access Token URL',
|
||||
name: 'accessTokenUrl',
|
||||
type: 'hidden',
|
||||
default: 'https://slack.com/api/oauth.v2.access',
|
||||
},
|
||||
{
|
||||
displayName: 'Scope',
|
||||
name: 'scope',
|
||||
type: 'hidden',
|
||||
default: 'chat:write',
|
||||
},
|
||||
{
|
||||
displayName: 'Auth URI Query Parameters',
|
||||
name: 'authQueryParameters',
|
||||
type: 'hidden',
|
||||
default:
|
||||
'user_scope=channels:read channels:write chat:write files:read files:write groups:read im:read mpim:read reactions:read reactions:write stars:read stars:write usergroups:write usergroups:read users.profile:read users.profile:write users:read',
|
||||
},
|
||||
{
|
||||
displayName: 'Authentication',
|
||||
name: 'authentication',
|
||||
type: 'hidden',
|
||||
default: 'body',
|
||||
},
|
||||
{
|
||||
displayName:
|
||||
'If you get an Invalid Scopes error, make sure you add the correct one <a target="_blank" href="https://docs.n8n.io/integrations/builtin/credentials/slack/#using-oauth">here</a> to your Slack integration',
|
||||
name: 'notice',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
iconUrl: 'icons/n8n-nodes-base/dist/nodes/Slack/slack.svg',
|
||||
supportedNodes: ['n8n-nodes-base.slack'],
|
||||
},
|
||||
};
|
||||
|
||||
export const PERSONAL_OPENAI_CREDENTIAL = TEST_CREDENTIALS[1];
|
||||
export const PROJECT_OPENAI_CREDENTIAL = TEST_CREDENTIALS[4];
|
|
@ -0,0 +1,78 @@
|
|||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import { mockedStore } from '@/__tests__/utils';
|
||||
import { useCredentialsStore } from '@/stores/credentials.store';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import CredentialPicker from './CredentialPicker.vue';
|
||||
import {
|
||||
PERSONAL_OPENAI_CREDENTIAL,
|
||||
PROJECT_OPENAI_CREDENTIAL,
|
||||
TEST_CREDENTIAL_TYPES,
|
||||
TEST_CREDENTIALS,
|
||||
} from './CredentialPicker.test.constants';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { screen } from '@testing-library/vue';
|
||||
|
||||
vi.mock('vue-router', () => {
|
||||
const push = vi.fn();
|
||||
const resolve = vi.fn().mockReturnValue({ href: 'https://test.com' });
|
||||
return {
|
||||
useRouter: () => ({
|
||||
push,
|
||||
resolve,
|
||||
}),
|
||||
useRoute: () => ({}),
|
||||
RouterLink: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
let credentialsStore: ReturnType<typeof mockedStore<typeof useCredentialsStore>>;
|
||||
|
||||
const renderComponent = createComponentRenderer(CredentialPicker);
|
||||
|
||||
describe('CredentialPicker', () => {
|
||||
beforeEach(() => {
|
||||
createTestingPinia();
|
||||
credentialsStore = mockedStore(useCredentialsStore);
|
||||
credentialsStore.state.credentials = TEST_CREDENTIALS;
|
||||
credentialsStore.state.credentialTypes = TEST_CREDENTIAL_TYPES;
|
||||
});
|
||||
|
||||
it('should render', () => {
|
||||
expect(() =>
|
||||
renderComponent({
|
||||
props: {
|
||||
appName: 'OpenAI',
|
||||
credentialType: 'openAiApi',
|
||||
selectedCredentialId: null,
|
||||
},
|
||||
}),
|
||||
).not.toThrowError();
|
||||
});
|
||||
|
||||
it('should only render personal credentials of the specified type', async () => {
|
||||
const TEST_APP_NAME = 'OpenAI';
|
||||
const TEST_CREDENTIAL_TYPE = 'openAiApi';
|
||||
const { getByTestId } = renderComponent({
|
||||
props: {
|
||||
appName: TEST_APP_NAME,
|
||||
credentialType: TEST_CREDENTIAL_TYPE,
|
||||
selectedCredentialId: null,
|
||||
},
|
||||
});
|
||||
expect(getByTestId('credential-dropdown')).toBeInTheDocument();
|
||||
expect(getByTestId('credential-dropdown')).toHaveAttribute(
|
||||
'credential-type',
|
||||
TEST_CREDENTIAL_TYPE,
|
||||
);
|
||||
// Open the dropdown
|
||||
await userEvent.click(getByTestId('credential-dropdown'));
|
||||
// Personal openAI credential should be in the dropdown
|
||||
expect(
|
||||
screen.getByTestId(`node-credentials-select-item-${PERSONAL_OPENAI_CREDENTIAL.id}`),
|
||||
).toBeInTheDocument();
|
||||
// OpenAI credential that belong to other project should not be in the dropdown
|
||||
expect(
|
||||
screen.queryByTestId(`node-credentials-select-item-${PROJECT_OPENAI_CREDENTIAL.id}`),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -26,7 +26,10 @@ const i18n = useI18n();
|
|||
const wasModalOpenedFromHere = ref(false);
|
||||
|
||||
const availableCredentials = computed(() => {
|
||||
return credentialsStore.getCredentialsByType(props.credentialType);
|
||||
const credByType = credentialsStore.getCredentialsByType(props.credentialType);
|
||||
// Only show personal credentials since templates are created in personal by default
|
||||
// Here, we don't care about sharing because credentials cannot be shared with personal project
|
||||
return credByType.filter((credential) => credential.homeProject?.type === 'personal');
|
||||
});
|
||||
|
||||
const credentialOptions = computed(() => {
|
||||
|
@ -98,6 +101,7 @@ listenForModalChanges({
|
|||
:credential-type="props.credentialType"
|
||||
:credential-options="credentialOptions"
|
||||
:selected-credential-id="props.selectedCredentialId"
|
||||
data-test-id="credential-dropdown"
|
||||
@credential-selected="onCredentialSelected"
|
||||
@new-credential="createNewCredential"
|
||||
/>
|
||||
|
|
Loading…
Reference in a new issue