first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
import type { CreateCredentialDto } from '@n8n/api-types';
|
||||
|
||||
import { test, expect } from '../../../fixtures/base';
|
||||
|
||||
test.describe('Credential API Operations', {
|
||||
annotation: [
|
||||
{ type: 'owner', description: 'Identity & Access' },
|
||||
],
|
||||
}, () => {
|
||||
test.describe('Basic CRUD Operations', () => {
|
||||
test('should create, retrieve, update, and delete credential', async ({ api }) => {
|
||||
const credentialData: CreateCredentialDto = {
|
||||
name: 'Test HTTP Basic Auth',
|
||||
type: 'httpBasicAuth',
|
||||
data: {
|
||||
user: 'test_user',
|
||||
password: 'test_password',
|
||||
},
|
||||
};
|
||||
|
||||
const { credentialId, createdCredential } =
|
||||
await api.credentials.createCredentialFromDefinition(credentialData);
|
||||
|
||||
expect(credentialId).toBeTruthy();
|
||||
expect(createdCredential.type).toBe('httpBasicAuth');
|
||||
expect(createdCredential.name).toContain('Test HTTP Basic Auth (Test');
|
||||
|
||||
const retrievedCredential = await api.credentials.getCredential(credentialId);
|
||||
expect(retrievedCredential.id).toBe(credentialId);
|
||||
expect(retrievedCredential.type).toBe('httpBasicAuth');
|
||||
expect(retrievedCredential.name).toBe(createdCredential.name);
|
||||
|
||||
const credentialWithData = await api.credentials.getCredential(credentialId, {
|
||||
includeData: true,
|
||||
});
|
||||
expect(credentialWithData.data).toBeDefined();
|
||||
expect(credentialWithData.data?.user).toBe('test_user');
|
||||
|
||||
const updatedName = 'Updated HTTP Basic Auth';
|
||||
const updatedCredential = await api.credentials.updateCredential(credentialId, {
|
||||
name: updatedName,
|
||||
data: {
|
||||
user: 'updated_user',
|
||||
password: 'updated_password',
|
||||
},
|
||||
});
|
||||
expect(updatedCredential.name).toBe(updatedName);
|
||||
|
||||
const verifyUpdated = await api.credentials.getCredential(credentialId, {
|
||||
includeData: true,
|
||||
});
|
||||
expect(verifyUpdated.name).toBe(updatedName);
|
||||
expect(verifyUpdated.data?.user).toBe('updated_user');
|
||||
|
||||
const deleteResult = await api.credentials.deleteCredential(credentialId);
|
||||
expect(deleteResult).toBe(true);
|
||||
|
||||
await expect(api.credentials.getCredential(credentialId)).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Credential Listing', () => {
|
||||
test('should list credentials with different query options', async ({ api }) => {
|
||||
const credential1 = await api.credentials.createCredentialFromDefinition({
|
||||
name: 'First Test Credential',
|
||||
type: 'httpBasicAuth',
|
||||
data: { user: 'user1', password: 'pass1' },
|
||||
});
|
||||
|
||||
const credential2 = await api.credentials.createCredentialFromDefinition({
|
||||
name: 'Second Test Credential',
|
||||
type: 'httpHeaderAuth',
|
||||
data: { name: 'Authorization', value: 'Bearer token' },
|
||||
});
|
||||
|
||||
const allCredentials = await api.credentials.getCredentials();
|
||||
expect(allCredentials.length).toBeGreaterThanOrEqual(2);
|
||||
|
||||
const createdIds = [credential1.credentialId, credential2.credentialId];
|
||||
const foundCredentials = allCredentials.filter((c) => createdIds.includes(c.id));
|
||||
expect(foundCredentials).toHaveLength(2);
|
||||
|
||||
const credentialsWithScopes = await api.credentials.getCredentials({
|
||||
includeGlobal: false,
|
||||
includeScopes: true,
|
||||
});
|
||||
expect(credentialsWithScopes[0].scopes).toBeDefined();
|
||||
expect(Array.isArray(credentialsWithScopes[0].scopes)).toBe(true);
|
||||
|
||||
const credentialsWithData = await api.credentials.getCredentials({
|
||||
includeGlobal: false,
|
||||
includeData: true,
|
||||
});
|
||||
const foundWithData = credentialsWithData.filter((c) => createdIds.includes(c.id));
|
||||
expect(foundWithData.some((c) => c.data)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Project Integration', () => {
|
||||
test('should handle credential-project associations', async ({ api }) => {
|
||||
await api.enableFeature('projectRole:admin');
|
||||
await api.enableFeature('projectRole:editor');
|
||||
await api.setMaxTeamProjectsQuota(-1);
|
||||
|
||||
const project = await api.projects.createProject('Test Project for Credentials');
|
||||
|
||||
const credential = await api.credentials.createCredentialFromDefinition({
|
||||
name: 'Project Credential',
|
||||
type: 'httpBasicAuth',
|
||||
data: { user: 'user', password: 'pass' },
|
||||
projectId: project.id,
|
||||
});
|
||||
|
||||
const projectCredentials = await api.credentials.getCredentialsForWorkflow({
|
||||
projectId: project.id,
|
||||
});
|
||||
|
||||
expect(projectCredentials).toBeDefined();
|
||||
expect(Array.isArray(projectCredentials)).toBe(true);
|
||||
|
||||
const foundCredential = projectCredentials.find((c) => c.id === credential.credentialId);
|
||||
expect(foundCredential).toBeDefined();
|
||||
});
|
||||
|
||||
test('should transfer credential between projects', async ({ api }) => {
|
||||
await api.enableFeature('projectRole:admin');
|
||||
await api.enableFeature('projectRole:editor');
|
||||
await api.setMaxTeamProjectsQuota(-1);
|
||||
|
||||
const sourceProject = await api.projects.createProject('Source Project');
|
||||
const destinationProject = await api.projects.createProject('Destination Project');
|
||||
|
||||
const credential = await api.credentials.createCredentialFromDefinition({
|
||||
name: 'Transfer Test Credential',
|
||||
type: 'httpBasicAuth',
|
||||
data: { user: 'user', password: 'pass' },
|
||||
projectId: sourceProject.id,
|
||||
});
|
||||
|
||||
const sourceCredentials = await api.credentials.getCredentialsForWorkflow({
|
||||
projectId: sourceProject.id,
|
||||
});
|
||||
const foundInSource = sourceCredentials.find((c) => c.id === credential.credentialId);
|
||||
expect(foundInSource).toBeDefined();
|
||||
|
||||
await api.credentials.transferCredential(credential.credentialId, destinationProject.id);
|
||||
|
||||
const destinationCredentials = await api.credentials.getCredentialsForWorkflow({
|
||||
projectId: destinationProject.id,
|
||||
});
|
||||
const foundInDestination = destinationCredentials.find(
|
||||
(c) => c.id === credential.credentialId,
|
||||
);
|
||||
expect(foundInDestination).toBeDefined();
|
||||
|
||||
const sourceCredentialsAfter = await api.credentials.getCredentialsForWorkflow({
|
||||
projectId: sourceProject.id,
|
||||
});
|
||||
const stillInSource = sourceCredentialsAfter.find((c) => c.id === credential.credentialId);
|
||||
expect(stillInSource).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Data Persistence', () => {
|
||||
test('should maintain credential data across operations', async ({ api }) => {
|
||||
const originalData: CreateCredentialDto = {
|
||||
name: 'Persistence Test Credential',
|
||||
type: 'httpBasicAuth',
|
||||
data: {
|
||||
user: 'persistent_user',
|
||||
password: 'persistent_password',
|
||||
},
|
||||
};
|
||||
|
||||
const { credentialId } = await api.credentials.createCredentialFromDefinition(originalData);
|
||||
|
||||
const afterCreate = await api.credentials.getCredential(credentialId, {
|
||||
includeData: true,
|
||||
});
|
||||
expect(afterCreate.data?.user).toBe('persistent_user');
|
||||
|
||||
await api.credentials.updateCredential(credentialId, {
|
||||
data: {
|
||||
user: 'updated_persistent_user',
|
||||
password: 'updated_persistent_password',
|
||||
},
|
||||
});
|
||||
|
||||
const afterUpdate = await api.credentials.getCredential(credentialId, {
|
||||
includeData: true,
|
||||
});
|
||||
expect(afterUpdate.data?.user).toBe('updated_persistent_user');
|
||||
expect(afterUpdate.data?.password).toBeDefined();
|
||||
|
||||
const allCredentials = await api.credentials.getCredentials();
|
||||
const foundCredential = allCredentials.find((c) => c.id === credentialId);
|
||||
expect(foundCredential).toBeDefined();
|
||||
expect(foundCredential!.type).toBe('httpBasicAuth');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,351 @@
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { test, expect } from '../../../fixtures/base';
|
||||
|
||||
test.describe(
|
||||
'Credentials',
|
||||
{
|
||||
annotation: [{ type: 'owner', description: 'Identity & Access' }],
|
||||
},
|
||||
() => {
|
||||
test.beforeEach(async ({ n8n }) => {
|
||||
await n8n.goHome();
|
||||
});
|
||||
|
||||
test('should create a new credential using empty state', async ({ n8n }) => {
|
||||
const projectId = await n8n.start.fromNewProject();
|
||||
const credentialName = `My awesome Notion account ${nanoid()}`;
|
||||
|
||||
await n8n.credentialsComposer.createFromList(
|
||||
'Notion API',
|
||||
{ apiKey: '1234567890' },
|
||||
{ name: credentialName, projectId },
|
||||
);
|
||||
|
||||
await expect(n8n.credentials.cards.getCredentials()).toHaveCount(1);
|
||||
await expect(n8n.credentials.cards.getCredential(credentialName)).toBeVisible();
|
||||
});
|
||||
|
||||
test('should sort credentials', async ({ n8n }) => {
|
||||
const projectId = await n8n.start.fromNewProject();
|
||||
const credentialA = `A Credential ${nanoid()}`;
|
||||
const credentialZ = `Z Credential ${nanoid()}`;
|
||||
|
||||
await n8n.api.credentials.createCredential({
|
||||
name: credentialA,
|
||||
type: 'notionApi',
|
||||
data: { apiKey: '1234567890' },
|
||||
projectId,
|
||||
});
|
||||
|
||||
await n8n.api.credentials.createCredential({
|
||||
name: credentialZ,
|
||||
type: 'trelloApi',
|
||||
data: { apiKey: 'test_api_key', apiToken: 'test_api_token' },
|
||||
projectId,
|
||||
});
|
||||
|
||||
await n8n.navigate.toCredentials(projectId);
|
||||
await n8n.credentials.clearSearch();
|
||||
await n8n.credentials.sortByNameDescending();
|
||||
|
||||
const firstCardDescending = n8n.credentials.cards.getCredentials().first();
|
||||
await expect(firstCardDescending).toContainText(credentialZ);
|
||||
|
||||
await n8n.credentials.sortByNameAscending();
|
||||
|
||||
const firstCardAscending = n8n.credentials.cards.getCredentials().first();
|
||||
await expect(firstCardAscending).toContainText(credentialA);
|
||||
});
|
||||
|
||||
test('should create credentials from NDV for node with multiple auth options', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
await n8n.start.fromNewProjectBlankCanvas();
|
||||
const credentialName = `My Google Service Account ${nanoid()}`;
|
||||
|
||||
await n8n.canvas.addNode('Manual Trigger');
|
||||
await n8n.canvas.addNode('Gmail', { action: 'Send a message' });
|
||||
|
||||
await n8n.ndv.clickCreateNewCredential();
|
||||
|
||||
// Gmail has 2 auth options (OAuth2 + Service Account), shown as a dropdown
|
||||
await expect(n8n.canvas.credentialModal.getModeSelector()).toBeVisible();
|
||||
|
||||
await n8n.canvas.credentialModal.selectAuthTypeFromDropdown(/Service Account/);
|
||||
|
||||
// Fill in the Service Account fields and save
|
||||
await n8n.canvas.credentialModal.addCredential(
|
||||
{
|
||||
email: 'test@project.iam.gserviceaccount.com',
|
||||
privateKey: 'test_private_key',
|
||||
},
|
||||
{ name: credentialName },
|
||||
);
|
||||
|
||||
await expect(n8n.ndv.getCredentialSelect()).toHaveValue(credentialName);
|
||||
});
|
||||
|
||||
test('should show multiple credential types in the same dropdown', async ({ n8n }) => {
|
||||
const projectId = await n8n.start.fromNewProjectBlankCanvas();
|
||||
const serviceAccountCredentialName2 = `OAuth2 Credential ${nanoid()}`;
|
||||
const serviceAccountCredentialName = `Service Account Credential ${nanoid()}`;
|
||||
|
||||
await n8n.api.credentials.createCredential({
|
||||
name: serviceAccountCredentialName2,
|
||||
type: 'googleApi',
|
||||
data: { email: 'test@service.com', privateKey: 'test_key' },
|
||||
projectId,
|
||||
});
|
||||
|
||||
await n8n.api.credentials.createCredential({
|
||||
name: serviceAccountCredentialName,
|
||||
type: 'googleApi',
|
||||
data: { email: 'test@service.com', privateKey: 'test_key' },
|
||||
projectId,
|
||||
});
|
||||
|
||||
await n8n.canvas.addNode('Manual Trigger');
|
||||
await n8n.canvas.addNode('Gmail', { action: 'Send a message' });
|
||||
|
||||
await n8n.ndv.getCredentialSelect().click();
|
||||
await expect(n8n.ndv.getCredentialOptionByText(serviceAccountCredentialName2)).toBeVisible();
|
||||
await expect(n8n.ndv.getCredentialOptionByText(serviceAccountCredentialName)).toBeVisible();
|
||||
await expect(n8n.ndv.credentialDropdownCreateNewCredential()).toBeVisible();
|
||||
await expect(n8n.ndv.getCredentialDropdownOptions()).toHaveCount(2);
|
||||
});
|
||||
|
||||
test('should correctly render required and optional credentials', async ({ n8n }) => {
|
||||
await n8n.start.fromNewProjectBlankCanvas();
|
||||
|
||||
await n8n.canvas.addNode('Pipedrive', { trigger: 'On new Pipedrive event' });
|
||||
await n8n.ndv.selectOptionInParameterDropdown('incomingAuthentication', 'Basic Auth');
|
||||
await expect(n8n.ndv.getNodeCredentialsSelect()).toHaveCount(2);
|
||||
|
||||
await n8n.ndv.clickCreateNewCredential(0);
|
||||
// First credential type has multiple auth options → mode selector visible
|
||||
await expect(n8n.canvas.credentialModal.getModeSelector()).toBeVisible();
|
||||
await n8n.canvas.credentialModal.close();
|
||||
|
||||
await n8n.ndv.clickCreateNewCredential(1);
|
||||
await expect(n8n.canvas.credentialModal.getModal()).toBeVisible();
|
||||
// Second credential type has single auth option → no mode selector
|
||||
await expect(n8n.canvas.credentialModal.getModeSelector()).toBeHidden();
|
||||
await n8n.canvas.credentialModal.close();
|
||||
});
|
||||
|
||||
test('should create credentials from NDV for node with no auth options', async ({ n8n }) => {
|
||||
await n8n.start.fromNewProjectBlankCanvas();
|
||||
const credentialName = `My Trello Account ${nanoid()}`;
|
||||
|
||||
await n8n.canvas.addNode('Manual Trigger');
|
||||
await n8n.canvas.addNode('Trello', { action: 'Create a card' });
|
||||
|
||||
await n8n.credentialsComposer.createFromNdv(
|
||||
{
|
||||
apiKey: 'test_api_key',
|
||||
apiToken: 'test_api_token',
|
||||
},
|
||||
{ name: credentialName },
|
||||
);
|
||||
|
||||
await expect(n8n.ndv.getCredentialSelect()).toHaveValue(credentialName);
|
||||
});
|
||||
|
||||
test('should delete credentials from NDV', async ({ n8n }) => {
|
||||
await n8n.start.fromNewProjectBlankCanvas();
|
||||
const credentialName = `Notion Credential ${nanoid()}`;
|
||||
|
||||
await n8n.canvas.addNode('Manual Trigger');
|
||||
await n8n.canvas.addNode('Notion', { action: 'Append a block' });
|
||||
|
||||
await n8n.credentialsComposer.createFromNdv(
|
||||
{ apiKey: '1234567890' },
|
||||
{ name: credentialName },
|
||||
);
|
||||
await expect(n8n.ndv.getCredentialSelect()).toHaveValue(credentialName);
|
||||
|
||||
await n8n.canvas.credentialModal.editCredential();
|
||||
await n8n.canvas.credentialModal.deleteCredential();
|
||||
await n8n.canvas.credentialModal.confirmDelete();
|
||||
|
||||
await expect(
|
||||
n8n.notifications.getNotificationByTitleOrContent('Credential deleted'),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(n8n.ndv.getCredentialSelect()).not.toHaveValue(credentialName);
|
||||
});
|
||||
|
||||
test('should rename credentials from NDV', async ({ n8n }) => {
|
||||
await n8n.start.fromNewProjectBlankCanvas();
|
||||
const initialName = `My Trello Account ${nanoid()}`;
|
||||
const renamedName = `Something else ${nanoid()}`;
|
||||
|
||||
await n8n.canvas.addNode('Manual Trigger');
|
||||
await n8n.canvas.addNode('Trello', { action: 'Create a card' });
|
||||
|
||||
await n8n.credentialsComposer.createFromNdv(
|
||||
{
|
||||
apiKey: 'test_api_key',
|
||||
apiToken: 'test_api_token',
|
||||
},
|
||||
{ name: initialName },
|
||||
);
|
||||
|
||||
await n8n.canvas.credentialModal.editCredential();
|
||||
await n8n.canvas.credentialModal.renameCredential(renamedName);
|
||||
await n8n.canvas.credentialModal.save();
|
||||
await n8n.canvas.credentialModal.close();
|
||||
|
||||
await expect(n8n.ndv.getCredentialSelect()).toHaveValue(renamedName);
|
||||
});
|
||||
|
||||
test('should edit credential for non-standard credential type', async ({ n8n }) => {
|
||||
await n8n.start.fromNewProjectBlankCanvas();
|
||||
const initialName = `Adalo Credential ${nanoid()}`;
|
||||
const editedName = `Something else ${nanoid()}`;
|
||||
|
||||
await n8n.canvas.addNode('AI Agent', { closeNDV: true });
|
||||
await n8n.canvas.addNode('HTTP Request Tool');
|
||||
|
||||
await n8n.ndv.selectOptionInParameterDropdown('authentication', 'Predefined Credential Type');
|
||||
await n8n.ndv.selectOptionInParameterDropdown('nodeCredentialType', 'Adalo API');
|
||||
|
||||
await n8n.credentialsComposer.createFromNdv(
|
||||
{
|
||||
apiKey: 'test_adalo_key',
|
||||
appId: 'test_app_id',
|
||||
},
|
||||
{ name: initialName },
|
||||
);
|
||||
|
||||
await n8n.canvas.credentialModal.editCredential();
|
||||
await n8n.canvas.credentialModal.renameCredential(editedName);
|
||||
await n8n.canvas.credentialModal.save();
|
||||
await n8n.canvas.credentialModal.close();
|
||||
|
||||
await expect(n8n.ndv.getCredentialSelect()).toHaveValue(editedName);
|
||||
});
|
||||
|
||||
test('should set a default credential when adding nodes', async ({ n8n }) => {
|
||||
const projectId = await n8n.start.fromNewProjectBlankCanvas();
|
||||
const credentialName = `My awesome Notion account ${nanoid()}`;
|
||||
|
||||
await n8n.api.credentials.createCredential({
|
||||
name: credentialName,
|
||||
type: 'notionApi',
|
||||
data: { apiKey: '1234567890' },
|
||||
projectId,
|
||||
});
|
||||
|
||||
await n8n.canvas.addNode('Manual Trigger');
|
||||
await n8n.canvas.addNode('Notion', { action: 'Append a block' });
|
||||
await expect(n8n.ndv.getCredentialSelect()).toHaveValue(credentialName);
|
||||
|
||||
const credentials = await n8n.api.credentials.getCredentials();
|
||||
const credential = credentials.find((c) => c.name === credentialName);
|
||||
await n8n.api.credentials.deleteCredential(credential!.id);
|
||||
});
|
||||
|
||||
test('should set a default credential when editing a node', async ({ n8n }) => {
|
||||
const projectId = await n8n.start.fromNewProjectBlankCanvas();
|
||||
const credentialName = `My awesome Notion account ${nanoid()}`;
|
||||
|
||||
await n8n.api.credentials.createCredential({
|
||||
name: credentialName,
|
||||
type: 'notionApi',
|
||||
data: { apiKey: '1234567890' },
|
||||
projectId,
|
||||
});
|
||||
|
||||
await n8n.canvas.addNode('Manual Trigger');
|
||||
await n8n.canvas.addNode('HTTP Request');
|
||||
|
||||
await n8n.ndv.selectOptionInParameterDropdown('authentication', 'Predefined Credential Type');
|
||||
await n8n.ndv.selectOptionInParameterDropdown('nodeCredentialType', 'Notion API');
|
||||
await expect(n8n.ndv.getCredentialSelect()).toHaveValue(credentialName);
|
||||
|
||||
const credentials = await n8n.api.credentials.getCredentials();
|
||||
const credential = credentials.find((c) => c.name === credentialName);
|
||||
await n8n.api.credentials.deleteCredential(credential!.id);
|
||||
});
|
||||
|
||||
test('should setup generic authentication for HTTP node', async ({ n8n }) => {
|
||||
await n8n.start.fromNewProjectBlankCanvas();
|
||||
const credentialName = `Query Auth Credential ${nanoid()}`;
|
||||
|
||||
await n8n.canvas.addNode('Manual Trigger');
|
||||
await n8n.canvas.addNode('HTTP Request');
|
||||
|
||||
await n8n.ndv.selectOptionInParameterDropdown('authentication', 'Generic Credential Type');
|
||||
await n8n.ndv.selectOptionInParameterDropdown('genericAuthType', 'Query Auth');
|
||||
|
||||
await n8n.credentialsComposer.createFromNdv(
|
||||
{
|
||||
name: 'api_key',
|
||||
value: 'test_query_value',
|
||||
},
|
||||
{ name: credentialName },
|
||||
);
|
||||
|
||||
await expect(n8n.ndv.getCredentialSelect()).toHaveValue(credentialName);
|
||||
});
|
||||
|
||||
test('should not show OAuth redirect URL section when OAuth2 credentials are overridden', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
// Mock credential types response to simulate admin override
|
||||
await n8n.page.route('**/rest/types/credentials.json', async (route) => {
|
||||
const response = await route.fetch();
|
||||
const json = await response.json();
|
||||
|
||||
// Override Slack OAuth2 credential properties
|
||||
if (json.slackOAuth2Api) {
|
||||
json.slackOAuth2Api.__overwrittenProperties = ['clientId', 'clientSecret'];
|
||||
}
|
||||
|
||||
await route.fulfill({ json });
|
||||
});
|
||||
|
||||
await n8n.start.fromNewProjectBlankCanvas();
|
||||
|
||||
await n8n.canvas.addNode('Manual Trigger');
|
||||
await n8n.canvas.addNode('Slack', { action: 'Get a channel' });
|
||||
|
||||
await n8n.ndv.clickCreateNewCredential();
|
||||
|
||||
// With overridden OAuth2 properties, the mode selector shows a dropdown
|
||||
// with Managed OAuth2 selected by default — redirect URL should be hidden
|
||||
await expect(n8n.canvas.credentialModal.getModeSelector()).toBeVisible();
|
||||
await expect(n8n.canvas.credentialModal.getOAuthRedirectUrl()).toBeHidden();
|
||||
await expect(n8n.canvas.credentialModal.getModal()).toBeVisible();
|
||||
});
|
||||
|
||||
test('ADO-2583 should show notifications above credential modal overlay', async ({ n8n }) => {
|
||||
await n8n.page.route('**/rest/credentials', async (route) => {
|
||||
if (route.request().method() === 'POST') {
|
||||
await route.abort('failed');
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
const projectId = await n8n.start.fromNewProject();
|
||||
await n8n.navigate.toCredentials(projectId);
|
||||
await n8n.credentials.addResource.credential();
|
||||
await n8n.credentials.selectCredentialType('Notion API');
|
||||
await n8n.canvas.credentialModal.fillField('apiKey', '1234567890');
|
||||
|
||||
const saveBtn = n8n.canvas.credentialModal.getSaveButton();
|
||||
await saveBtn.click();
|
||||
|
||||
const errorNotification = n8n.notifications.getErrorNotifications();
|
||||
await expect(errorNotification).toBeVisible();
|
||||
await expect(n8n.canvas.credentialModal.getModal()).toBeVisible();
|
||||
|
||||
const modalOverlay = n8n.page.locator('.el-overlay').first();
|
||||
await expect(errorNotification).toHaveCSS('z-index', '2100');
|
||||
await expect(modalOverlay).toHaveCSS('z-index', '2001');
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,160 @@
|
||||
import { test, expect } from '../../../fixtures/base';
|
||||
|
||||
test.use({ capability: { env: { TEST_ISOLATION: 'global-credentials' } } });
|
||||
|
||||
test.describe('Global credentials', {
|
||||
annotation: [
|
||||
{ type: 'owner', description: 'Identity & Access' },
|
||||
],
|
||||
}, () => {
|
||||
test.describe.configure({ mode: 'serial' });
|
||||
|
||||
test.beforeAll(async ({ api }) => {
|
||||
await api.enableFeature('sharing');
|
||||
});
|
||||
|
||||
test('owner should create HTTP header credential and set to global', async ({ n8n }) => {
|
||||
await n8n.api.signin('owner');
|
||||
|
||||
// Navigate to credentials page
|
||||
await n8n.navigate.toCredentials();
|
||||
|
||||
// Create new credential
|
||||
await n8n.credentials.addResource.credential();
|
||||
await n8n.credentials.selectCredentialType('Header Auth');
|
||||
|
||||
// Fill in credential fields
|
||||
await n8n.credentials.credentialModal.fillField('name', 'Authorization');
|
||||
await n8n.credentials.credentialModal.fillField('value', 'Bearer test-token-123');
|
||||
|
||||
// Set credential name
|
||||
await n8n.credentials.credentialModal.getCredentialName().click();
|
||||
await n8n.credentials.credentialModal.getNameInput().fill('Global HTTP Header Cred');
|
||||
|
||||
// Switch to Sharing tab
|
||||
await n8n.credentials.credentialModal.changeTab('Sharing');
|
||||
|
||||
// Share with all users (set to global)
|
||||
await n8n.credentials.credentialModal.getUsersSelect().click();
|
||||
await n8n.credentials.credentialModal.getVisibleDropdown().getByText('All users').click();
|
||||
|
||||
// Save the credential with sharing
|
||||
await n8n.credentials.credentialModal.save();
|
||||
await n8n.credentials.credentialModal.close();
|
||||
|
||||
// Verify credential appears in list with global badge
|
||||
await expect(n8n.credentials.cards.getCredential('Global HTTP Header Cred')).toBeVisible();
|
||||
await expect(
|
||||
n8n.credentials.cards
|
||||
.getCredential('Global HTTP Header Cred')
|
||||
.getByTestId('credential-global-badge'),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('member should see global credential in credentials view', async ({ n8n }) => {
|
||||
await n8n.api.signin('member', 0);
|
||||
|
||||
// Navigate to credentials page
|
||||
await n8n.navigate.toCredentials();
|
||||
|
||||
// Verify global credential is visible to member
|
||||
await expect(n8n.credentials.cards.getCredential('Global HTTP Header Cred')).toBeVisible();
|
||||
|
||||
// Verify global badge is displayed
|
||||
await expect(
|
||||
n8n.credentials.cards
|
||||
.getCredential('Global HTTP Header Cred')
|
||||
.getByTestId('credential-global-badge'),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('member should execute workflow with HTTP node using global credential', async ({
|
||||
n8n,
|
||||
baseURL,
|
||||
}) => {
|
||||
await n8n.api.signin('member', 0);
|
||||
|
||||
// Create a new workflow
|
||||
await n8n.navigate.toWorkflow('new');
|
||||
await n8n.canvas.setWorkflowName('Test Global Credential Workflow');
|
||||
|
||||
// Add manual trigger and HTTP Request node
|
||||
await n8n.canvas.addNode('Manual Trigger');
|
||||
await n8n.canvas.addNode('HTTP Request');
|
||||
|
||||
await n8n.ndv.fillParameterInput('URL', `${baseURL}/rest/settings`);
|
||||
await n8n.ndv.selectOptionInParameterDropdown('authentication', 'Generic Credential Type');
|
||||
await n8n.ndv.selectOptionInParameterDropdown('genericAuthType', 'Header Auth');
|
||||
|
||||
// Verify global credential is available in the credential select
|
||||
const credentialSelect = n8n.ndv.getCredentialSelect();
|
||||
await credentialSelect.click();
|
||||
|
||||
// Check that global credential appears in dropdown
|
||||
const dropdown = n8n.credentials.credentialModal.getVisibleDropdown();
|
||||
await expect(dropdown.getByText('Global HTTP Header Cred')).toBeVisible();
|
||||
|
||||
// Select the global credential
|
||||
await dropdown.getByText('Global HTTP Header Cred').click();
|
||||
|
||||
// Verify credential is selected
|
||||
await expect(credentialSelect).toHaveValue('Global HTTP Header Cred');
|
||||
|
||||
// Close NDV
|
||||
await n8n.ndv.clickBackToCanvasButton();
|
||||
|
||||
await n8n.workflowComposer.executeWorkflowAndWaitForNotification(
|
||||
'Workflow executed successfully',
|
||||
);
|
||||
});
|
||||
|
||||
test('owner should be able to remove global sharing', async ({ n8n }) => {
|
||||
await n8n.api.signin('owner');
|
||||
|
||||
// Navigate to credentials page
|
||||
await n8n.navigate.toCredentials();
|
||||
|
||||
// Open the global credential
|
||||
await n8n.credentials.cards.getCredential('Global HTTP Header Cred').click();
|
||||
|
||||
// Switch to Sharing tab
|
||||
await n8n.credentials.credentialModal.changeTab('Sharing');
|
||||
|
||||
// Verify "All users" is in the sharing list
|
||||
await expect(
|
||||
n8n.credentials.credentialModal
|
||||
.getModal()
|
||||
.getByTestId('project-sharing-list-item')
|
||||
.filter({ hasText: 'All users' }),
|
||||
).toBeVisible();
|
||||
|
||||
// Remove global sharing by clicking the remove button
|
||||
await n8n.credentials.credentialModal
|
||||
.getModal()
|
||||
.getByTestId('project-sharing-list-item')
|
||||
.filter({ hasText: 'All users' })
|
||||
.getByTestId('project-sharing-remove')
|
||||
.click();
|
||||
|
||||
// Save the changes
|
||||
await n8n.credentials.credentialModal.save();
|
||||
await n8n.credentials.credentialModal.close();
|
||||
|
||||
// Verify global badge is no longer visible
|
||||
await expect(
|
||||
n8n.credentials.cards
|
||||
.getCredential('Global HTTP Header Cred')
|
||||
.getByTestId('credential-global-badge'),
|
||||
).toBeHidden();
|
||||
});
|
||||
|
||||
test('member should not see credential after global sharing removed', async ({ n8n }) => {
|
||||
await n8n.api.signin('member', 0);
|
||||
|
||||
// Navigate to credentials page
|
||||
await n8n.navigate.toCredentials();
|
||||
|
||||
// Verify credential is no longer visible to member
|
||||
await expect(n8n.credentials.cards.getCredential('Global HTTP Header Cred')).toBeHidden();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import { test, expect } from '../../../fixtures/base';
|
||||
|
||||
test.describe(
|
||||
'OAuth Credentials',
|
||||
{
|
||||
annotation: [{ type: 'owner', description: 'Identity & Access' }],
|
||||
},
|
||||
() => {
|
||||
test('should create and connect with Google OAuth2', async ({ n8n }) => {
|
||||
const projectId = await n8n.start.fromNewProjectBlankCanvas();
|
||||
await n8n.navigate.toCredentials(projectId);
|
||||
await n8n.credentials.emptyListCreateCredentialButton.click();
|
||||
await n8n.credentials.createCredentialFromCredentialPicker(
|
||||
'Google OAuth2 API',
|
||||
{
|
||||
clientId: 'test-key',
|
||||
clientSecret: 'test-secret',
|
||||
},
|
||||
{ closeDialog: false, skipSave: true },
|
||||
);
|
||||
|
||||
const popupPromise = n8n.page.waitForEvent('popup');
|
||||
await n8n.credentials.credentialModal.oauthConnectButton.click();
|
||||
|
||||
const popup = await popupPromise;
|
||||
const popupUrl = popup.url();
|
||||
expect(popupUrl).toContain('accounts.google.com');
|
||||
expect(popupUrl).toContain('client_id=test-key');
|
||||
|
||||
await popup.close();
|
||||
|
||||
await n8n.page.evaluate(() => {
|
||||
const channel = new BroadcastChannel('oauth-callback');
|
||||
channel.postMessage('success');
|
||||
});
|
||||
|
||||
await expect(n8n.credentials.credentialModal.oauthConnectSuccessBanner).toContainText(
|
||||
'Account connected',
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user