mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-12 16:10:30 +02:00
test: Migrate sharing spec from Cypress to Playwright (#21024)
This commit is contained in:
parent
3caa5ac3a5
commit
e3bb2f5c68
127
.github/workflows/e2e-reusable.yml
vendored
127
.github/workflows/e2e-reusable.yml
vendored
|
|
@ -1,127 +0,0 @@
|
|||
name: Reusable e2e workflow
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
branch:
|
||||
description: 'GitHub branch to test.'
|
||||
required: false
|
||||
type: string
|
||||
user:
|
||||
description: 'User who kicked this off.'
|
||||
required: false
|
||||
type: string
|
||||
default: 'schedule'
|
||||
spec:
|
||||
description: 'Specify specs.'
|
||||
required: false
|
||||
default: 'e2e/*'
|
||||
type: string
|
||||
parallel:
|
||||
description: 'Run tests in parallel.'
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
containers:
|
||||
description: 'Number of containers to run tests in.'
|
||||
required: false
|
||||
default: '[1, 2]'
|
||||
type: string
|
||||
pr_number:
|
||||
description: 'PR number to run tests for.'
|
||||
required: false
|
||||
type: number
|
||||
secrets:
|
||||
CURRENTS_RECORD_KEY:
|
||||
description: 'Currents record key.'
|
||||
required: true
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=3072
|
||||
|
||||
jobs:
|
||||
testing:
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2204
|
||||
outputs:
|
||||
dashboardUrl: ${{ steps.cypress.outputs.dashboardUrl }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# If spec is not e2e/* then we run only one container to prevent
|
||||
# running the same tests multiple times
|
||||
containers: ${{ fromJSON( inputs.spec == 'e2e/*' && inputs.containers || '[1]' ) }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.branch || github.ref }}
|
||||
- name: Set up and build
|
||||
uses: ./.github/actions/setup-nodejs-blacksmith
|
||||
|
||||
- name: Install Cypress
|
||||
working-directory: cypress
|
||||
run: pnpm cypress:install
|
||||
|
||||
- name: Cypress run
|
||||
id: cypress
|
||||
uses: cypress-io/github-action@be1bab96b388bbd9ce3887e397d373c8557e15af # v6.9.2
|
||||
with:
|
||||
working-directory: cypress
|
||||
install: false
|
||||
start: pnpm start
|
||||
wait-on: 'http://localhost:5678'
|
||||
wait-on-timeout: 120
|
||||
record: false
|
||||
spec: ${{ inputs.spec == 'e2e/*' && format('e2e/group{0}/**/*.cy.ts', matrix.containers) || inputs.spec }}
|
||||
env:
|
||||
NODE_OPTIONS: --dns-result-order=ipv4first
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
E2E_TESTS: true
|
||||
COMMIT_INFO_MESSAGE: 🌳 ${{ inputs.branch }} 🤖 ${{ inputs.user }} 🗃️ ${{ inputs.spec }}
|
||||
SHELL: /bin/sh
|
||||
|
||||
- name: Upload test results artifact
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: test-results-${{ matrix.containers }}
|
||||
path: cypress/test-results-*.xml
|
||||
|
||||
upload-to-currents:
|
||||
needs: testing
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Download all test results
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
path: test-results
|
||||
|
||||
- name: Merge and upload to Currents
|
||||
run: |
|
||||
npm install -g @currents/cmd junit-report-merger
|
||||
# Merge all XML files, so Currents can show a single view for Cypress
|
||||
jrm combined-results.xml "test-results/**/test-results-*.xml"
|
||||
|
||||
- name: Upload merged XML as artifact
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: merged-junit-results
|
||||
path: combined-results.xml
|
||||
|
||||
- name: Convert and upload to Currents
|
||||
run: |
|
||||
currents convert \
|
||||
--input-format=junit \
|
||||
--input-file=combined-results.xml \
|
||||
--output-dir=.currents \
|
||||
--framework=node \
|
||||
--framework-version=cypress-14.4.0
|
||||
|
||||
currents upload \
|
||||
--project-id=I0yzoc \
|
||||
--key=${{ secrets.CURRENTS_RECORD_KEY }} \
|
||||
--ci-build-id=n8n-io/n8n-${{ github.run_id }}-${{ github.run_attempt }} \
|
||||
--report-dir=.currents \
|
||||
--tag=cypress
|
||||
14
.github/workflows/e2e-tests-pr.yml
vendored
14
.github/workflows/e2e-tests-pr.yml
vendored
|
|
@ -16,16 +16,6 @@ jobs:
|
|||
with:
|
||||
is_pr_approved_by_maintainer: true
|
||||
|
||||
run-e2e-tests:
|
||||
name: E2E
|
||||
uses: ./.github/workflows/e2e-reusable.yml
|
||||
needs: [eligibility_check]
|
||||
if: needs.eligibility_check.outputs.should_run == 'true'
|
||||
with:
|
||||
pr_number: ${{ github.event.pull_request.number }}
|
||||
user: ${{ github.event.pull_request.user.login || 'PR User' }}
|
||||
secrets: inherit
|
||||
|
||||
run-playwright-tests:
|
||||
name: Playwright
|
||||
uses: ./.github/workflows/playwright-test-reusable.yml
|
||||
|
|
@ -36,9 +26,9 @@ jobs:
|
|||
post-e2e-tests:
|
||||
name: E2E - Checks
|
||||
runs-on: ubuntu-latest
|
||||
needs: [eligibility_check, run-e2e-tests, run-playwright-tests]
|
||||
needs: [eligibility_check, run-playwright-tests]
|
||||
if: always() && needs.eligibility_check.result != 'skipped'
|
||||
steps:
|
||||
- name: Fail if tests failed
|
||||
if: needs.run-e2e-tests.result == 'failure' || needs.run-playwright-tests.result == 'failure'
|
||||
if: needs.run-playwright-tests.result == 'failure'
|
||||
run: exit 1
|
||||
|
|
|
|||
16
.github/workflows/e2e-tests.yml
vendored
16
.github/workflows/e2e-tests.yml
vendored
|
|
@ -10,11 +10,6 @@ on:
|
|||
description: 'GitHub branch to test.'
|
||||
required: false
|
||||
default: 'master'
|
||||
spec:
|
||||
description: 'Specify specs.'
|
||||
required: false
|
||||
default: 'e2e/*'
|
||||
type: string
|
||||
user:
|
||||
description: 'User who kicked this off.'
|
||||
required: false
|
||||
|
|
@ -41,15 +36,6 @@ jobs:
|
|||
[[ "${{ env.START_URL }}" != "" ]] && curl -v -X POST -d 'url=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}' "${{ env.START_URL }}" || echo ""
|
||||
shell: bash
|
||||
|
||||
run-e2e-tests:
|
||||
name: E2E
|
||||
uses: ./.github/workflows/e2e-reusable.yml
|
||||
with:
|
||||
branch: ${{ github.event.inputs.branch || 'master' }}
|
||||
user: ${{ github.event.inputs.user || 'PR User' }}
|
||||
spec: ${{ github.event.inputs.spec || 'e2e/*' }}
|
||||
secrets: inherit
|
||||
|
||||
run-playwright-tests:
|
||||
name: Playwright
|
||||
uses: ./.github/workflows/playwright-test-reusable.yml
|
||||
|
|
@ -60,7 +46,7 @@ jobs:
|
|||
calls-success-url-notify:
|
||||
name: Calls success URL and notifies
|
||||
runs-on: ubuntu-latest
|
||||
needs: [run-e2e-tests, run-playwright-tests]
|
||||
needs: [run-playwright-tests]
|
||||
if: ${{ github.event.inputs.success-url != '' }}
|
||||
steps:
|
||||
- name: Notify Slack on failure
|
||||
|
|
|
|||
|
|
@ -1,249 +0,0 @@
|
|||
import {
|
||||
addItemToFixedCollection,
|
||||
assertNodeOutputHintExists,
|
||||
clickExecuteNode,
|
||||
clickGetBackToCanvas,
|
||||
getExecuteNodeButton,
|
||||
getOutputTableHeaders,
|
||||
getParameterInputByName,
|
||||
populateFixedCollection,
|
||||
selectResourceLocatorItem,
|
||||
selectResourceLocatorAddResourceItem,
|
||||
typeIntoFixedCollectionItem,
|
||||
clickWorkflowCardContent,
|
||||
assertOutputTableContent,
|
||||
populateMapperFields,
|
||||
getNodeRunInfoStale,
|
||||
assertNodeOutputErrorMessageExists,
|
||||
checkParameterCheckboxInputByName,
|
||||
uncheckParameterCheckboxInputByName,
|
||||
} from '../../composables/ndv';
|
||||
import {
|
||||
clickExecuteWorkflowButton,
|
||||
clickZoomToFit,
|
||||
navigateToNewWorkflowPage,
|
||||
openNode,
|
||||
pasteWorkflow,
|
||||
saveWorkflowOnButtonClick,
|
||||
} from '../../composables/workflow';
|
||||
import { visitWorkflowsPage } from '../../composables/workflowsPage';
|
||||
import SUB_WORKFLOW_INPUTS from '../../fixtures/Test_Subworkflow-Inputs.json';
|
||||
import { errorToast, successToast } from '../../pages/notifications';
|
||||
import { getVisiblePopper } from '../../utils';
|
||||
|
||||
const DEFAULT_WORKFLOW_NAME = 'My workflow';
|
||||
const DEFAULT_SUBWORKFLOW_NAME_1 = 'My Sub-Workflow 1';
|
||||
const DEFAULT_SUBWORKFLOW_NAME_2 = 'My Sub-Workflow 2';
|
||||
|
||||
const EXAMPLE_FIELDS = [
|
||||
['aNumber', 'Number'],
|
||||
['aString', 'String'],
|
||||
['aArray', 'Array'],
|
||||
['aObject', 'Object'],
|
||||
['aAny', 'Allow Any Type'],
|
||||
// bool last because it's a switch instead of a normal inputField so we'll skip it for some cases
|
||||
['aBool', 'Boolean'],
|
||||
] as const;
|
||||
|
||||
type TypeField = 'Allow Any Type' | 'String' | 'Number' | 'Boolean' | 'Array' | 'Object';
|
||||
|
||||
// eslint-disable-next-line n8n-local-rules/no-skipped-tests
|
||||
describe.skip('Sub-workflow creation and typed usage', () => {
|
||||
beforeEach(() => {
|
||||
navigateToNewWorkflowPage();
|
||||
pasteWorkflow(SUB_WORKFLOW_INPUTS);
|
||||
saveWorkflowOnButtonClick();
|
||||
clickZoomToFit();
|
||||
|
||||
openNode('Execute Workflow');
|
||||
|
||||
let openedUrl = '';
|
||||
|
||||
// Prevent sub-workflow from opening in new window
|
||||
cy.window().then((win) => {
|
||||
cy.stub(win, 'open').callsFake((url) => {
|
||||
openedUrl = url;
|
||||
});
|
||||
});
|
||||
selectResourceLocatorAddResourceItem('workflowId', 'Create a');
|
||||
cy.then(() => cy.visit(openedUrl));
|
||||
// **************************
|
||||
// NAVIGATE TO CHILD WORKFLOW
|
||||
// **************************
|
||||
// Close NDV before opening the node creator
|
||||
clickGetBackToCanvas();
|
||||
openNode('When Executed by Another Workflow');
|
||||
});
|
||||
|
||||
it('works with type-checked values', () => {
|
||||
populateFixedCollection(EXAMPLE_FIELDS, 'workflowInputs', 1);
|
||||
|
||||
validateAndReturnToParent(
|
||||
DEFAULT_SUBWORKFLOW_NAME_1,
|
||||
1,
|
||||
EXAMPLE_FIELDS.map((f) => f[0]),
|
||||
);
|
||||
|
||||
const values = [
|
||||
'-1', // number fields don't support `=` switch to expression, so let's test the Fixed case with it
|
||||
...EXAMPLE_FIELDS.slice(1).map((x) => `={{}{{} $json.a${x[0]}`), // the `}}` at the end are added automatically
|
||||
];
|
||||
|
||||
// this matches with the pinned data provided in the fixture
|
||||
populateMapperFields(values.map((x, i) => [EXAMPLE_FIELDS[i][0], x]));
|
||||
|
||||
clickExecuteNode();
|
||||
|
||||
const expected = [
|
||||
['-1', 'A String', '0:11:true2:3', 'aKey:-1', '[empty object]', 'true'],
|
||||
['-1', 'Another String', '[empty array]', 'aDifferentKey:-1', '[empty array]', 'true'],
|
||||
];
|
||||
assertOutputTableContent(expected);
|
||||
|
||||
// Test the type-checking options
|
||||
populateMapperFields([['aString', '{selectAll}{backspace}{{}{{} 5']]);
|
||||
|
||||
getNodeRunInfoStale().should('exist');
|
||||
clickExecuteNode();
|
||||
|
||||
assertNodeOutputErrorMessageExists();
|
||||
|
||||
// attemptToConvertTypes enabled
|
||||
checkParameterCheckboxInputByName('attemptToConvertTypes');
|
||||
|
||||
getNodeRunInfoStale().should('exist');
|
||||
clickExecuteNode();
|
||||
|
||||
const expected2 = [
|
||||
['-1', '5', '0:11:true2:3', 'aKey:-1', '[empty object]', 'true'],
|
||||
['-1', '5', '[empty array]', 'aDifferentKey:-1', '[empty array]', 'true'],
|
||||
];
|
||||
|
||||
assertOutputTableContent(expected2);
|
||||
|
||||
// disabled again
|
||||
uncheckParameterCheckboxInputByName('attemptToConvertTypes');
|
||||
|
||||
getNodeRunInfoStale().should('exist');
|
||||
clickExecuteNode();
|
||||
|
||||
assertNodeOutputErrorMessageExists();
|
||||
});
|
||||
|
||||
it('works with Fields input source, and can then be changed to JSON input source', () => {
|
||||
assertNodeOutputHintExists();
|
||||
|
||||
populateFixedCollection(EXAMPLE_FIELDS, 'workflowInputs', 1);
|
||||
|
||||
validateAndReturnToParent(
|
||||
DEFAULT_SUBWORKFLOW_NAME_1,
|
||||
1,
|
||||
EXAMPLE_FIELDS.map((f) => f[0]),
|
||||
);
|
||||
|
||||
cy.window().then((win) => {
|
||||
cy.stub(win, 'open').callsFake((url) => {
|
||||
cy.visit(url);
|
||||
selectResourceLocatorAddResourceItem('workflowId', 'Create a');
|
||||
|
||||
openNode('When Executed by Another Workflow');
|
||||
|
||||
getParameterInputByName('inputSource').click();
|
||||
|
||||
getVisiblePopper()
|
||||
.getByTestId('parameter-input')
|
||||
.eq(0)
|
||||
.type('Using JSON Example{downArrow}{enter}');
|
||||
|
||||
const exampleJson =
|
||||
'{{}' + EXAMPLE_FIELDS.map((x) => `"${x[0]}": ${makeExample(x[1])}`).join(',') + '}';
|
||||
getParameterInputByName('jsonExample')
|
||||
.find('.cm-line')
|
||||
.eq(0)
|
||||
.type(`{selectAll}{backspace}${exampleJson}{enter}`);
|
||||
|
||||
// first one doesn't work for some reason, might need to wait for something?
|
||||
clickExecuteNode();
|
||||
|
||||
validateAndReturnToParent(
|
||||
DEFAULT_SUBWORKFLOW_NAME_2,
|
||||
2,
|
||||
EXAMPLE_FIELDS.map((f) => f[0]),
|
||||
);
|
||||
|
||||
assertOutputTableContent([
|
||||
['[null]', '[null]', '[null]', '[null]', '[null]', 'true'],
|
||||
['[null]', '[null]', '[null]', '[null]', '[null]', 'true'],
|
||||
]);
|
||||
|
||||
clickExecuteNode();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// eslint-disable-next-line n8n-local-rules/no-skipped-tests
|
||||
it.skip('should show node issue when no fields are defined in manual mode', () => {
|
||||
getExecuteNodeButton().should('be.disabled');
|
||||
clickGetBackToCanvas();
|
||||
// Executing the workflow should show an error toast
|
||||
clickExecuteWorkflowButton();
|
||||
errorToast().should('contain', 'The workflow has issues');
|
||||
openNode('When Executed by Another Workflow');
|
||||
// Add a field to the workflowInputs fixedCollection
|
||||
addItemToFixedCollection('workflowInputs');
|
||||
typeIntoFixedCollectionItem('workflowInputs', 0, 'test');
|
||||
// Executing the workflow should not show error now
|
||||
clickGetBackToCanvas();
|
||||
clickExecuteWorkflowButton();
|
||||
successToast().should('contain', 'Workflow executed successfully');
|
||||
});
|
||||
});
|
||||
|
||||
// This function starts off in the Child Workflow Input Trigger, assuming we just defined the input fields
|
||||
// It then navigates back to the parent and validates the outputPanel matches our changes
|
||||
function validateAndReturnToParent(targetChild: string, offset: number, fields: string[]) {
|
||||
clickExecuteNode();
|
||||
|
||||
// + 1 to account for formatting-only column
|
||||
getOutputTableHeaders().should('have.length', fields.length + 1);
|
||||
for (const [i, name] of fields.entries()) {
|
||||
getOutputTableHeaders().eq(i).should('have.text', name);
|
||||
}
|
||||
|
||||
clickGetBackToCanvas();
|
||||
saveWorkflowOnButtonClick();
|
||||
|
||||
visitWorkflowsPage();
|
||||
|
||||
clickWorkflowCardContent(DEFAULT_WORKFLOW_NAME);
|
||||
|
||||
openNode('Execute Workflow');
|
||||
|
||||
// Note that outside of e2e tests this will be pre-selected correctly.
|
||||
// Due to our workaround to remain in the same tab we need to select the correct tab manually
|
||||
selectResourceLocatorItem('workflowId', offset - 1, targetChild);
|
||||
|
||||
clickExecuteNode();
|
||||
|
||||
getOutputTableHeaders().should('have.length', fields.length + 1);
|
||||
for (const [i, name] of fields.entries()) {
|
||||
getOutputTableHeaders().eq(i).should('have.text', name);
|
||||
}
|
||||
}
|
||||
|
||||
function makeExample(type: TypeField) {
|
||||
switch (type) {
|
||||
case 'String':
|
||||
return '"example"';
|
||||
case 'Number':
|
||||
return '42';
|
||||
case 'Boolean':
|
||||
return 'true';
|
||||
case 'Array':
|
||||
return '["example", 123, null]';
|
||||
case 'Object':
|
||||
return '{{}"example": [123]}';
|
||||
case 'Allow Any Type':
|
||||
return 'null';
|
||||
}
|
||||
}
|
||||
|
|
@ -1,428 +0,0 @@
|
|||
import * as credentialsComposables from '../../composables/credentialsComposables';
|
||||
import { saveCredential } from '../../composables/modals/credential-modal';
|
||||
import * as projects from '../../composables/projects';
|
||||
import {
|
||||
INSTANCE_MEMBERS,
|
||||
INSTANCE_OWNER,
|
||||
INSTANCE_ADMIN,
|
||||
NOTION_NODE_NAME,
|
||||
} from '../../constants';
|
||||
import {
|
||||
CredentialsModal,
|
||||
CredentialsPage,
|
||||
NDV,
|
||||
WorkflowPage,
|
||||
WorkflowSharingModal,
|
||||
WorkflowsPage,
|
||||
} from '../../pages';
|
||||
import { getVisibleDropdown, getVisiblePopper, getVisibleSelect } from '../../utils';
|
||||
|
||||
/**
|
||||
* User U1 - Instance owner
|
||||
* User U2 - User, owns C1, W1, W2
|
||||
* User U3 - User, owns C2
|
||||
*
|
||||
* W1 - Workflow owned by User U2, shared with User U3
|
||||
* W2 - Workflow owned by User U2
|
||||
*
|
||||
* C1 - Credential owned by User U2
|
||||
* C2 - Credential owned by User U3, shared with User U1 and User U2
|
||||
*/
|
||||
|
||||
const credentialsPage = new CredentialsPage();
|
||||
const credentialsModal = new CredentialsModal();
|
||||
|
||||
const workflowsPage = new WorkflowsPage();
|
||||
const workflowPage = new WorkflowPage();
|
||||
const workflowSharingModal = new WorkflowSharingModal();
|
||||
const ndv = new NDV();
|
||||
|
||||
describe('Sharing', { disableAutoLogin: true }, () => {
|
||||
before(() => cy.enableFeature('sharing'));
|
||||
|
||||
let workflowW2Url = '';
|
||||
it('should create C1, W1, W2, share W1 with U3, as U2', () => {
|
||||
cy.signinAsMember(0);
|
||||
|
||||
credentialsComposables.loadCredentialsPage(credentialsPage.url);
|
||||
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
||||
credentialsModal.getters.newCredentialTypeOption('Notion API').click();
|
||||
credentialsModal.getters.newCredentialTypeButton().click();
|
||||
credentialsModal.getters.connectionParameter('Internal Integration Secret').type('1234567890');
|
||||
credentialsModal.actions.setName('Credential C1');
|
||||
credentialsModal.actions.save();
|
||||
credentialsModal.actions.close();
|
||||
|
||||
workflowPage.actions.visit();
|
||||
workflowPage.actions.setWorkflowName('Workflow W1');
|
||||
workflowPage.actions.addInitialNodeToCanvas('Manual Trigger');
|
||||
workflowPage.actions.addNodeToCanvas('Notion', true, true);
|
||||
ndv.getters.credentialInput().find('input').should('have.value', 'Credential C1');
|
||||
ndv.actions.close();
|
||||
|
||||
workflowPage.actions.openShareModal();
|
||||
workflowSharingModal.actions.addUser(INSTANCE_MEMBERS[1].email);
|
||||
workflowSharingModal.actions.save();
|
||||
workflowPage.actions.saveWorkflowOnButtonClick();
|
||||
|
||||
cy.visit(workflowsPage.url);
|
||||
workflowsPage.getters.createWorkflowButton().click();
|
||||
cy.createFixtureWorkflow('Test_workflow_1.json');
|
||||
workflowPage.actions.setWorkflowName('Workflow W2');
|
||||
cy.url().then((url) => {
|
||||
workflowW2Url = url;
|
||||
});
|
||||
});
|
||||
|
||||
it('should create C2, share C2 with U1 and U2, as U3', () => {
|
||||
cy.signinAsMember(1);
|
||||
|
||||
credentialsComposables.loadCredentialsPage(credentialsPage.url);
|
||||
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
||||
credentialsModal.getters.newCredentialTypeOption('Airtable Personal Access Token API').click();
|
||||
credentialsModal.getters.newCredentialTypeButton().click();
|
||||
credentialsModal.getters.connectionParameter('Access Token').type('1234567890');
|
||||
credentialsModal.actions.setName('Credential C2');
|
||||
credentialsModal.actions.changeTab('Sharing');
|
||||
credentialsModal.actions.addUser(INSTANCE_OWNER.email);
|
||||
credentialsModal.actions.addUser(INSTANCE_MEMBERS[0].email);
|
||||
credentialsModal.actions.save();
|
||||
credentialsModal.actions.close();
|
||||
});
|
||||
|
||||
it('should open W1, add node using C2 as U3', () => {
|
||||
cy.signinAsMember(1);
|
||||
|
||||
cy.visit(workflowsPage.url);
|
||||
workflowsPage.getters.workflowCards().should('have.length', 1);
|
||||
workflowsPage.getters.workflowCardContent('Workflow W1').click();
|
||||
workflowPage.actions.addNodeToCanvas('Airtable', true, true);
|
||||
ndv.getters.credentialInput().find('input').should('have.value', 'Credential C2');
|
||||
ndv.actions.close();
|
||||
workflowPage.actions.saveWorkflowOnButtonClick();
|
||||
|
||||
workflowPage.actions.openNode('Append a block');
|
||||
ndv.getters.credentialInput().should('have.value', 'Credential C1').should('be.disabled');
|
||||
ndv.actions.close();
|
||||
});
|
||||
|
||||
it('should open W1, add node using C2 as U2', () => {
|
||||
cy.signinAsMember(0);
|
||||
|
||||
cy.visit(workflowsPage.url);
|
||||
workflowsPage.getters.workflowCards().should('have.length', 2);
|
||||
workflowsPage.getters.workflowCardContent('Workflow W1').click();
|
||||
workflowPage.actions.addNodeToCanvas('Airtable', true, true);
|
||||
ndv.getters.credentialInput().find('input').should('have.value', 'Credential C2');
|
||||
ndv.actions.close();
|
||||
workflowPage.actions.saveWorkflowOnButtonClick();
|
||||
|
||||
workflowPage.actions.openNode('Append a block');
|
||||
ndv.getters
|
||||
.credentialInput()
|
||||
.find('input')
|
||||
.should('have.value', 'Credential C1')
|
||||
.should('be.enabled');
|
||||
ndv.actions.close();
|
||||
});
|
||||
|
||||
it('should not have access to W2, as U3', () => {
|
||||
cy.signinAsMember(1);
|
||||
|
||||
cy.visit(workflowW2Url);
|
||||
cy.waitForLoad();
|
||||
cy.location('pathname', { timeout: 10000 }).should('eq', '/entity-not-authorized/workflow');
|
||||
});
|
||||
|
||||
it('should have access to W1, W2, as U1', () => {
|
||||
cy.signinAsOwner();
|
||||
|
||||
cy.visit(workflowsPage.url);
|
||||
workflowsPage.getters.workflowCards().should('have.length', 2);
|
||||
workflowsPage.getters.workflowCardContent('Workflow W1').click();
|
||||
workflowPage.actions.openNode('Append a block');
|
||||
ndv.getters
|
||||
.credentialInput()
|
||||
.find('input')
|
||||
.should('have.value', 'Credential C1')
|
||||
.should('be.enabled');
|
||||
ndv.actions.close();
|
||||
|
||||
cy.waitForLoad();
|
||||
cy.visit(workflowsPage.url);
|
||||
workflowsPage.getters.workflowCardContent('Workflow W2').click('top');
|
||||
workflowPage.actions.executeWorkflow();
|
||||
});
|
||||
|
||||
it('should automatically test C2 when opened by U2 sharee', () => {
|
||||
cy.signinAsMember(0);
|
||||
|
||||
credentialsComposables.loadCredentialsPage(credentialsPage.url);
|
||||
credentialsPage.getters.credentialCard('Credential C2').click();
|
||||
credentialsModal.getters.testSuccessTag().should('be.visible');
|
||||
});
|
||||
|
||||
it('should work for admin role on credentials created by others (also can share it with themselves)', () => {
|
||||
cy.signinAsMember(0);
|
||||
|
||||
credentialsComposables.loadCredentialsPage(credentialsPage.url);
|
||||
credentialsPage.getters.createCredentialButton().click();
|
||||
credentialsModal.getters.newCredentialTypeOption('Notion API').click();
|
||||
credentialsModal.getters.newCredentialTypeButton().click({ force: true });
|
||||
credentialsModal.getters.connectionParameter('Internal Integration Secret').type('1234567890');
|
||||
credentialsModal.actions.setName('Credential C3');
|
||||
credentialsModal.actions.save();
|
||||
credentialsModal.actions.close();
|
||||
|
||||
cy.signout();
|
||||
cy.signinAsAdmin();
|
||||
credentialsComposables.loadCredentialsPage(credentialsPage.url);
|
||||
credentialsPage.getters.credentialCard('Credential C3').click();
|
||||
credentialsModal.getters.testSuccessTag().should('be.visible');
|
||||
cy.get('input').should('not.have.length');
|
||||
credentialsModal.actions.changeTab('Sharing');
|
||||
cy.contains(
|
||||
'Sharing a credential allows people to use it in their workflows. They cannot access credential details.',
|
||||
).should('be.visible');
|
||||
|
||||
credentialsModal.getters.usersSelect().click();
|
||||
getVisiblePopper()
|
||||
.find('[data-test-id="project-sharing-info"]')
|
||||
.filter(':visible')
|
||||
.should('have.length', 3)
|
||||
.contains(INSTANCE_ADMIN.email)
|
||||
.should('have.length', 1);
|
||||
getVisibleSelect().contains(INSTANCE_OWNER.email.toLowerCase()).click();
|
||||
|
||||
credentialsModal.actions.addUser(INSTANCE_MEMBERS[1].email);
|
||||
credentialsModal.actions.addUser(INSTANCE_ADMIN.email);
|
||||
credentialsModal.actions.saveSharing();
|
||||
credentialsModal.actions.close();
|
||||
});
|
||||
|
||||
it('credentials should work between team and personal projects', () => {
|
||||
cy.resetDatabase();
|
||||
cy.enableFeature('sharing');
|
||||
cy.enableFeature('advancedPermissions');
|
||||
cy.enableFeature('projectRole:admin');
|
||||
cy.enableFeature('projectRole:editor');
|
||||
cy.changeQuota('maxTeamProjects', -1);
|
||||
|
||||
cy.signinAsOwner();
|
||||
cy.visit('/');
|
||||
|
||||
projects.createProject('Development');
|
||||
|
||||
projects.getHomeButton().click();
|
||||
workflowsPage.getters.newWorkflowButtonCard().click();
|
||||
projects.createWorkflow('Test_workflow_1.json', 'Test workflow');
|
||||
|
||||
projects.getHomeButton().click();
|
||||
projects.getProjectTabCredentials().click();
|
||||
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
||||
projects.createCredential('Notion API');
|
||||
|
||||
credentialsPage.getters.credentialCard('Notion API').click();
|
||||
credentialsModal.actions.changeTab('Sharing');
|
||||
credentialsModal.getters.usersSelect().click();
|
||||
getVisibleSelect()
|
||||
.find('li')
|
||||
.should('have.length', 4)
|
||||
.filter(':contains("Development")')
|
||||
.should('have.length', 1)
|
||||
.click();
|
||||
saveCredential();
|
||||
credentialsModal.actions.close();
|
||||
|
||||
projects.getProjectTabWorkflows().click();
|
||||
workflowsPage.getters.workflowCardActions('Test workflow').click();
|
||||
getVisibleDropdown().find('li').contains('Share').click();
|
||||
|
||||
workflowSharingModal.getters.usersSelect().filter(':visible').click();
|
||||
getVisibleSelect().find('li').should('have.length', 3).first().click();
|
||||
workflowSharingModal.getters.saveButton().click();
|
||||
|
||||
projects.getMenuItems().first().click();
|
||||
workflowsPage.getters.newWorkflowButtonCard().click();
|
||||
projects.createWorkflow('Test_workflow_1.json', 'Test workflow 2');
|
||||
workflowPage.actions.openShareModal();
|
||||
workflowSharingModal.getters.usersSelect().should('not.exist');
|
||||
|
||||
cy.get('body').type('{esc}');
|
||||
|
||||
projects.getMenuItems().first().click();
|
||||
projects.getProjectTabCredentials().click();
|
||||
credentialsPage.getters.createCredentialButton().click();
|
||||
projects.createCredential('Notion API 2', false);
|
||||
credentialsModal.actions.changeTab('Sharing');
|
||||
credentialsModal.getters.usersSelect().click();
|
||||
getVisibleSelect().find('li').should('have.length', 4).first().click();
|
||||
saveCredential();
|
||||
credentialsModal.actions.close();
|
||||
|
||||
credentialsPage.getters
|
||||
.credentialCards()
|
||||
.should('have.length', 2)
|
||||
.filter(':contains("Personal")')
|
||||
.should('have.length', 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Credential Usage in Cross Shared Workflows', () => {
|
||||
beforeEach(() => {
|
||||
cy.resetDatabase();
|
||||
cy.enableFeature('sharing');
|
||||
cy.enableFeature('advancedPermissions');
|
||||
cy.enableFeature('projectRole:admin');
|
||||
cy.enableFeature('projectRole:editor');
|
||||
cy.changeQuota('maxTeamProjects', -1);
|
||||
cy.reload();
|
||||
cy.signinAsOwner();
|
||||
credentialsComposables.loadCredentialsPage(credentialsPage.url);
|
||||
});
|
||||
|
||||
it('should only show credentials from the same team project', () => {
|
||||
// Create a notion credential in the home project
|
||||
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
||||
credentialsModal.actions.createNewCredential('Notion API');
|
||||
|
||||
// Create a notion credential in one project
|
||||
projects.createProject('Development');
|
||||
projects.getProjectTabCredentials().click();
|
||||
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
||||
credentialsModal.actions.createNewCredential('Notion API');
|
||||
|
||||
// Create a notion credential in another project
|
||||
projects.createProject('Test');
|
||||
projects.getProjectTabCredentials().click();
|
||||
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
||||
credentialsModal.actions.createNewCredential('Notion API');
|
||||
// Create a workflow with a notion node in the same project
|
||||
projects.getProjectTabWorkflows().click();
|
||||
workflowsPage.actions.createWorkflowFromCard();
|
||||
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true);
|
||||
|
||||
// Only the credential in this project should be in the dropdown
|
||||
workflowPage.getters.nodeCredentialsSelect().click();
|
||||
getVisibleSelect().find('li').should('have.length', 1);
|
||||
});
|
||||
|
||||
it('should only show credentials in their personal project for members', () => {
|
||||
// Create a notion credential as the owner
|
||||
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
||||
credentialsModal.actions.createNewCredential('Notion API');
|
||||
|
||||
// Create another notion credential as the owner, but share it with member
|
||||
// 0
|
||||
credentialsPage.getters.createCredentialButton().click();
|
||||
credentialsModal.actions.createNewCredential('Notion API', false);
|
||||
credentialsModal.actions.changeTab('Sharing');
|
||||
credentialsModal.actions.addUser(INSTANCE_MEMBERS[0].email);
|
||||
credentialsModal.actions.saveSharing();
|
||||
|
||||
// As the member, create a new notion credential and a workflow
|
||||
cy.signinAsMember();
|
||||
credentialsComposables.loadCredentialsPage(credentialsPage.url);
|
||||
credentialsPage.getters.createCredentialButton().click();
|
||||
credentialsModal.actions.createNewCredential('Notion API');
|
||||
cy.visit(workflowsPage.url);
|
||||
workflowsPage.actions.createWorkflowFromCard();
|
||||
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true);
|
||||
|
||||
// Only the own credential the shared one should be in the dropdown
|
||||
workflowPage.getters.nodeCredentialsSelect().click();
|
||||
getVisibleSelect().find('li').should('have.length', 2);
|
||||
});
|
||||
|
||||
it('should only show credentials in their personal project for members if the workflow was shared with them', () => {
|
||||
const workflowName = 'Test workflow';
|
||||
|
||||
// Create a notion credential as the owner and a workflow that is shared
|
||||
// with member 0
|
||||
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
||||
credentialsModal.actions.createNewCredential('Notion API');
|
||||
//cy.visit(workflowsPage.url);
|
||||
projects.getProjectTabWorkflows().click();
|
||||
workflowsPage.actions.createWorkflowFromCard();
|
||||
workflowPage.actions.setWorkflowName(workflowName);
|
||||
workflowPage.actions.openShareModal();
|
||||
workflowSharingModal.actions.addUser(INSTANCE_MEMBERS[0].email);
|
||||
workflowSharingModal.actions.save();
|
||||
|
||||
// As the member, create a new notion credential
|
||||
cy.signinAsMember();
|
||||
credentialsComposables.loadCredentialsPage(credentialsPage.url);
|
||||
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
||||
credentialsModal.actions.createNewCredential('Notion API');
|
||||
cy.visit(workflowsPage.url);
|
||||
workflowsPage.getters.workflowCardContent(workflowName).click();
|
||||
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true);
|
||||
|
||||
// Only the own credential the shared one should be in the dropdown
|
||||
workflowPage.getters.nodeCredentialsSelect().click();
|
||||
getVisibleSelect().find('li').should('have.length', 1);
|
||||
});
|
||||
|
||||
it("should show all credentials from all personal projects the workflow's been shared into for the global owner", () => {
|
||||
const workflowName = 'Test workflow';
|
||||
|
||||
// As member 1, create a new notion credential. This should not show up.
|
||||
cy.signinAsMember(1);
|
||||
credentialsComposables.loadCredentialsPage(credentialsPage.url);
|
||||
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
||||
credentialsModal.actions.createNewCredential('Notion API');
|
||||
|
||||
// As admin, create a new notion credential. This should show up.
|
||||
cy.signinAsAdmin();
|
||||
credentialsComposables.loadCredentialsPage(credentialsPage.url);
|
||||
credentialsPage.getters.createCredentialButton().click();
|
||||
credentialsModal.actions.createNewCredential('Notion API');
|
||||
|
||||
// As member 0, create a new notion credential and a workflow and share it
|
||||
// with the global owner and the admin.
|
||||
cy.signinAsMember();
|
||||
credentialsComposables.loadCredentialsPage(credentialsPage.url);
|
||||
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
||||
credentialsModal.actions.createNewCredential('Notion API');
|
||||
cy.visit(workflowsPage.url);
|
||||
workflowsPage.actions.createWorkflowFromCard();
|
||||
workflowPage.actions.setWorkflowName(workflowName);
|
||||
workflowPage.actions.openShareModal();
|
||||
workflowSharingModal.actions.addUser(INSTANCE_OWNER.email);
|
||||
workflowSharingModal.actions.addUser(INSTANCE_ADMIN.email);
|
||||
workflowSharingModal.actions.save();
|
||||
|
||||
// As the global owner, create a new notion credential and open the shared
|
||||
// workflow
|
||||
cy.signinAsOwner();
|
||||
credentialsComposables.loadCredentialsPage(credentialsPage.url);
|
||||
|
||||
credentialsPage.getters.createCredentialButton().click();
|
||||
credentialsModal.actions.createNewCredential('Notion API');
|
||||
cy.visit(workflowsPage.url);
|
||||
workflowsPage.getters.workflowCardContent(workflowName).click();
|
||||
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true);
|
||||
|
||||
// Only the personal credentials of the workflow owner and the global owner should show up.
|
||||
workflowPage.getters.nodeCredentialsSelect().click();
|
||||
getVisibleSelect().find('li').should('have.length', 3);
|
||||
});
|
||||
|
||||
it('should show all personal credentials if the global owner owns the workflow', () => {
|
||||
// As member 0, create a new notion credential.
|
||||
cy.signinAsMember();
|
||||
credentialsComposables.loadCredentialsPage(credentialsPage.url);
|
||||
credentialsPage.getters.emptyListCreateCredentialButton().click();
|
||||
credentialsModal.actions.createNewCredential('Notion API');
|
||||
|
||||
// As the global owner, create a workflow and add a notion node
|
||||
cy.signinAsOwner();
|
||||
cy.visit(workflowsPage.url);
|
||||
workflowsPage.actions.createWorkflowFromCard();
|
||||
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true);
|
||||
|
||||
// Show all personal credentials
|
||||
workflowPage.getters.nodeCredentialsSelect().click();
|
||||
getVisibleSelect().find('li').should('have.have.length', 1);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import type { Request } from '@playwright/test';
|
||||
import { expect } from '@playwright/test';
|
||||
import type { IWorkflowBase } from 'n8n-workflow';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import type { n8nPage } from '../pages/n8nPage';
|
||||
|
|
@ -142,6 +143,19 @@ export class WorkflowComposer {
|
|||
}
|
||||
|
||||
/**
|
||||
* Get workflow by name via API
|
||||
* @param workflowName - Name of the workflow to find
|
||||
* @returns Workflow object with id, name, and other properties
|
||||
*/
|
||||
async getWorkflowByName(workflowName: string): Promise<IWorkflowBase> {
|
||||
const response = await this.n8n.api.request.get('/rest/workflows', {
|
||||
params: new URLSearchParams({ filter: JSON.stringify({ name: workflowName }) }),
|
||||
});
|
||||
const workflows = await response.json();
|
||||
return workflows.data[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves a workflow to a different project or user.
|
||||
* @param workflowName - The name of the workflow to move
|
||||
* @param projectNameOrEmail - The destination project name or user email
|
||||
|
|
|
|||
61
packages/testing/playwright/fixtures/Test_workflow_1.json
Normal file
61
packages/testing/playwright/fixtures/Test_workflow_1.json
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
{
|
||||
"name": "Test workflow 1",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "a2f85497-260d-4489-a957-2b7d88e2f33d",
|
||||
"name": "On clicking 'execute'",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [220, 260]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Loop over input items and add a new field\n// called 'myNewField' to the JSON of each one\nfor (const item of $input.all()) {\n item.json.myNewField = 1;\n}\n\nreturn $input.all();"
|
||||
},
|
||||
"id": "9493d278-1ede-47c9-bedf-92ac3a737c65",
|
||||
"name": "Code",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 1,
|
||||
"position": [400, 260]
|
||||
}
|
||||
],
|
||||
"pinData": {},
|
||||
"connections": {
|
||||
"On clicking 'execute'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Code",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Code": {
|
||||
"main": [[]]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {},
|
||||
"hash": "a59c7b1c97b1741597afae0fcd43ebef",
|
||||
"id": 3,
|
||||
"meta": {
|
||||
"instanceId": "a5280676597d00ecd0ea712da7f9cf2ce90174a791a309112731f6e44d162f35"
|
||||
},
|
||||
"tags": [
|
||||
{
|
||||
"name": "some-tag-1",
|
||||
"createdAt": "2022-11-10T13:43:34.001Z",
|
||||
"updatedAt": "2022-11-10T13:43:34.001Z",
|
||||
"id": "6"
|
||||
},
|
||||
{
|
||||
"name": "some-tag-2",
|
||||
"createdAt": "2022-11-10T13:43:39.778Z",
|
||||
"updatedAt": "2022-11-10T13:43:39.778Z",
|
||||
"id": "7"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -307,6 +307,11 @@ export class CanvasPage extends BasePage {
|
|||
await responsePromise;
|
||||
}
|
||||
|
||||
async openShareModal(): Promise<void> {
|
||||
await this.clickByTestId('workflow-share-button');
|
||||
await this.page.getByTestId('workflowShare-modal').waitFor({ state: 'visible' });
|
||||
}
|
||||
|
||||
async clickZoomToFitButton(): Promise<void> {
|
||||
await this.clickByTestId('zoom-to-fit');
|
||||
}
|
||||
|
|
@ -517,10 +522,18 @@ export class CanvasPage extends BasePage {
|
|||
return this.page.getByTestId('node-creator-action-item');
|
||||
}
|
||||
|
||||
nodeCreatorCategoryItem(categoryName: string): Locator {
|
||||
return this.page.getByTestId('node-creator-category-item').getByText(categoryName);
|
||||
}
|
||||
|
||||
nodeCreatorCategoryItems(): Locator {
|
||||
return this.page.getByTestId('node-creator-category-item');
|
||||
}
|
||||
|
||||
getFirstAction(): Locator {
|
||||
return this.page.locator('[data-keyboard-nav-type="action"]').first();
|
||||
}
|
||||
|
||||
selectedNodes(): Locator {
|
||||
return this.page
|
||||
.locator('[data-test-id="canvas-node"]')
|
||||
|
|
|
|||
|
|
@ -9,6 +9,14 @@ export class WorkflowSharingModal extends BasePage {
|
|||
await this.getModal().waitFor({ state: 'visible', timeout: 5000 });
|
||||
}
|
||||
|
||||
getUsersSelect() {
|
||||
return this.page.getByTestId('project-sharing-select').filter({ visible: true });
|
||||
}
|
||||
|
||||
getVisibleDropdown() {
|
||||
return this.page.locator('.el-select-dropdown:visible');
|
||||
}
|
||||
|
||||
async addUser(email: string) {
|
||||
await this.clickByTestId('project-sharing-select');
|
||||
await this.page
|
||||
|
|
@ -19,6 +27,7 @@ export class WorkflowSharingModal extends BasePage {
|
|||
|
||||
async save() {
|
||||
await this.clickByTestId('workflow-sharing-modal-save-button');
|
||||
await this.getModal().waitFor({ state: 'hidden' });
|
||||
}
|
||||
|
||||
async close() {
|
||||
|
|
|
|||
|
|
@ -96,6 +96,10 @@ export class CredentialModal {
|
|||
return this.root.getByTestId('oauth-connect-success-banner');
|
||||
}
|
||||
|
||||
getTestSuccessTag(): Locator {
|
||||
return this.root.getByTestId('credentials-config-container-test-success');
|
||||
}
|
||||
|
||||
async editCredential(): Promise<void> {
|
||||
await this.root.page().getByTestId('credential-edit-button').click();
|
||||
}
|
||||
|
|
@ -125,4 +129,54 @@ export class CredentialModal {
|
|||
getAuthTypeRadioButtons() {
|
||||
return this.root.page().locator('label.el-radio');
|
||||
}
|
||||
|
||||
async changeTab(tabName: 'Sharing'): Promise<void> {
|
||||
await this.root.getByTestId('menu-item').filter({ hasText: tabName }).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the users select dropdown in the Sharing tab
|
||||
*/
|
||||
getUsersSelect(): Locator {
|
||||
return this.root.getByTestId('project-sharing-select').filter({ visible: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the visible dropdown popper (for sharing dropdown interactions)
|
||||
*/
|
||||
getVisibleDropdown(): Locator {
|
||||
return this.root.page().locator('.el-popper[aria-hidden="false"]');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a user to credential sharing
|
||||
* @param email - User email to share with
|
||||
*/
|
||||
async addUserToSharing(email: string): Promise<void> {
|
||||
await this.getUsersSelect().click();
|
||||
await this.getVisibleDropdown().getByText(email.toLowerCase(), { exact: false }).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save credential sharing (different from regular save - hits /share endpoint)
|
||||
*/
|
||||
async saveSharing(): Promise<void> {
|
||||
const saveBtn = this.getSaveButton();
|
||||
await saveBtn.click();
|
||||
|
||||
// Wait for share API call to complete
|
||||
await this.root
|
||||
.page()
|
||||
.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('/rest/credentials/') &&
|
||||
response.url().includes('/share') &&
|
||||
response.request().method() === 'PUT',
|
||||
);
|
||||
|
||||
await saveBtn.getByText('Saved', { exact: true }).waitFor({
|
||||
state: 'visible',
|
||||
timeout: 3000,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
import type { Page } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* ProjectTabs component - navigation tabs within a project view
|
||||
* Mirrors the ProjectTabs.vue component in the frontend
|
||||
*/
|
||||
export class ProjectTabsComponent {
|
||||
constructor(private readonly page: Page) {}
|
||||
|
||||
async clickCredentialsTab() {
|
||||
await this.page
|
||||
.getByTestId('project-tabs')
|
||||
.getByRole('link', { name: /credentials/i })
|
||||
.click();
|
||||
}
|
||||
|
||||
async clickWorkflowsTab() {
|
||||
await this.page
|
||||
.getByTestId('project-tabs')
|
||||
.getByRole('link', { name: /workflows/i })
|
||||
.click();
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import { CanvasPage } from './CanvasPage';
|
|||
import { CommunityNodesPage } from './CommunityNodesPage';
|
||||
import { BaseModal } from './components/BaseModal';
|
||||
import { Breadcrumbs } from './components/Breadcrumbs';
|
||||
import { ProjectTabsComponent } from './components/ProjectTabsComponent';
|
||||
import { ResourceMoveModal } from './components/ResourceMoveModal';
|
||||
import { CredentialsPage } from './CredentialsPage';
|
||||
import { DataTableDetails } from './DataTableDetails';
|
||||
|
|
@ -85,6 +86,10 @@ export class n8nPage {
|
|||
|
||||
readonly signIn: SignInPage;
|
||||
readonly settingsUsers: SettingsUsersPage;
|
||||
|
||||
// Components
|
||||
readonly projectTabs: ProjectTabsComponent;
|
||||
|
||||
readonly settingsEnvironment: SettingsEnvironmentPage;
|
||||
// Modals
|
||||
readonly workflowActivationModal: WorkflowActivationModal;
|
||||
|
|
@ -148,6 +153,10 @@ export class n8nPage {
|
|||
this.settingsEnvironment = new SettingsEnvironmentPage(page);
|
||||
|
||||
this.settingsUsers = new SettingsUsersPage(page);
|
||||
|
||||
// Components
|
||||
this.projectTabs = new ProjectTabsComponent(page);
|
||||
|
||||
// Modals
|
||||
this.workflowActivationModal = new WorkflowActivationModal(page);
|
||||
this.workflowCredentialSetupModal = new WorkflowCredentialSetupModal(page);
|
||||
|
|
|
|||
|
|
@ -18,6 +18,10 @@ const CONTAINER_ONLY = new RegExp(`@capability:(${CONTAINER_ONLY_TAGS.join('|')}
|
|||
// In local run they are a "dependency" which means they will be skipped if earlier tests fail, not ideal but needed for isolation
|
||||
const SERIAL_EXECUTION = /@db:reset/;
|
||||
|
||||
// Routes tests to isolated worker without triggering automatic database resets in fixtures
|
||||
// Use when tests need worker isolation but have intentional state dependencies (e.g., serial tests sharing data)
|
||||
const ISOLATED_ONLY = /@isolated/;
|
||||
|
||||
const CONTAINER_CONFIGS: Array<{ name: string; config: N8NConfig }> = [
|
||||
{ name: 'standard', config: {} },
|
||||
{ name: 'postgres', config: { postgres: true } },
|
||||
|
|
@ -34,21 +38,23 @@ export function getProjects(): Project[] {
|
|||
{
|
||||
name: 'ui',
|
||||
testDir: './tests/ui',
|
||||
grepInvert: new RegExp([CONTAINER_ONLY.source, SERIAL_EXECUTION.source].join('|')),
|
||||
grepInvert: new RegExp(
|
||||
[CONTAINER_ONLY.source, SERIAL_EXECUTION.source, ISOLATED_ONLY.source].join('|'),
|
||||
),
|
||||
fullyParallel: true,
|
||||
use: { baseURL: process.env.N8N_BASE_URL },
|
||||
},
|
||||
{
|
||||
name: 'ui:isolated',
|
||||
testDir: './tests/ui',
|
||||
grep: SERIAL_EXECUTION,
|
||||
grep: new RegExp([SERIAL_EXECUTION.source, ISOLATED_ONLY.source].join('|')),
|
||||
workers: 1,
|
||||
use: { baseURL: process.env.N8N_BASE_URL },
|
||||
},
|
||||
);
|
||||
} else {
|
||||
for (const { name, config } of CONTAINER_CONFIGS) {
|
||||
const grepInvertPatterns = [SERIAL_EXECUTION.source];
|
||||
const grepInvertPatterns = [SERIAL_EXECUTION.source, ISOLATED_ONLY.source];
|
||||
projects.push(
|
||||
{
|
||||
name: `${name}:ui`,
|
||||
|
|
@ -61,7 +67,7 @@ export function getProjects(): Project[] {
|
|||
{
|
||||
name: `${name}:ui:isolated`,
|
||||
testDir: './tests/ui',
|
||||
grep: SERIAL_EXECUTION,
|
||||
grep: new RegExp([SERIAL_EXECUTION.source, ISOLATED_ONLY.source].join('|')),
|
||||
workers: 1,
|
||||
use: { containerConfig: config },
|
||||
},
|
||||
|
|
|
|||
478
packages/testing/playwright/tests/ui/17-sharing.spec.ts
Normal file
478
packages/testing/playwright/tests/ui/17-sharing.spec.ts
Normal file
|
|
@ -0,0 +1,478 @@
|
|||
import { test, expect } from '../../fixtures/base';
|
||||
|
||||
const OWNER_EMAIL = 'nathan@n8n.io';
|
||||
const ADMIN_EMAIL = 'admin@n8n.io';
|
||||
const MEMBER_0_EMAIL = 'member@n8n.io'; // U2
|
||||
const MEMBER_1_EMAIL = 'member2@n8n.io'; // U3
|
||||
|
||||
const TEST_API_KEY = '1234567890';
|
||||
const TEST_ACCESS_TOKEN = '1234567890';
|
||||
|
||||
test.describe('@isolated', () => {
|
||||
test.describe.configure({ mode: 'serial' });
|
||||
|
||||
test.describe('Sharing - Workflow and Credential Sharing (Sequential)', () => {
|
||||
test.beforeAll(async ({ api }) => {
|
||||
await api.resetDatabase();
|
||||
await api.enableFeature('sharing');
|
||||
await api.enableFeature('advancedPermissions');
|
||||
await api.enableFeature('projectRole:admin');
|
||||
await api.enableFeature('projectRole:editor');
|
||||
});
|
||||
|
||||
test('should create C1, W1, W2, share W1 with U3, as U2', async ({ n8n }) => {
|
||||
await n8n.api.signin('member', 0);
|
||||
|
||||
await n8n.credentialsComposer.createFromList(
|
||||
'Notion API',
|
||||
{ apiKey: TEST_API_KEY },
|
||||
{ name: 'Credential C1' },
|
||||
);
|
||||
|
||||
await n8n.navigate.toWorkflow('new');
|
||||
await n8n.canvas.setWorkflowName('Workflow W1');
|
||||
await n8n.canvas.addNode('Manual Trigger');
|
||||
await n8n.canvas.addNode('Notion', { action: 'Append a block' });
|
||||
|
||||
// Verify C1 auto-selected
|
||||
await expect(n8n.ndv.getCredentialSelect()).toHaveValue('Credential C1');
|
||||
await n8n.ndv.clickBackToCanvasButton();
|
||||
|
||||
// Share W1 with U3 before saving
|
||||
await n8n.canvas.openShareModal();
|
||||
await n8n.workflowSharingModal.addUser(MEMBER_1_EMAIL);
|
||||
await n8n.workflowSharingModal.save();
|
||||
await n8n.canvas.saveWorkflow();
|
||||
|
||||
await n8n.navigate.toWorkflows();
|
||||
await n8n.workflows.addResource.workflow();
|
||||
await n8n.canvas.importWorkflow('Test_workflow_1.json', 'Workflow W2');
|
||||
});
|
||||
|
||||
test('should create C2, share C2 with U1 and U2, as U3', async ({ n8n }) => {
|
||||
await n8n.api.signin('member', 1);
|
||||
|
||||
// Manual approach to access Sharing tab during creation
|
||||
await n8n.navigate.toCredentials();
|
||||
await n8n.credentials.addResource.credential();
|
||||
await n8n.credentials.selectCredentialType('Airtable Personal Access Token API');
|
||||
|
||||
await n8n.credentials.credentialModal.fillField('accessToken', TEST_ACCESS_TOKEN);
|
||||
await n8n.credentials.credentialModal.getCredentialName().click();
|
||||
await n8n.credentials.credentialModal.getNameInput().fill('Credential C2');
|
||||
|
||||
await n8n.credentials.credentialModal.changeTab('Sharing');
|
||||
await n8n.credentials.credentialModal.addUserToSharing(OWNER_EMAIL);
|
||||
await n8n.credentials.credentialModal.addUserToSharing(MEMBER_0_EMAIL);
|
||||
await n8n.credentials.credentialModal.saveSharing();
|
||||
await n8n.credentials.credentialModal.close();
|
||||
});
|
||||
|
||||
test('should open W1, add node using C2 as U3', async ({ n8n }) => {
|
||||
await n8n.api.signin('member', 1);
|
||||
|
||||
await n8n.navigate.toWorkflows();
|
||||
|
||||
// U3 only sees W1 (not W2)
|
||||
await expect(n8n.workflows.cards.getWorkflows()).toHaveCount(1);
|
||||
await expect(n8n.workflows.cards.getWorkflow('Workflow W1')).toBeVisible();
|
||||
|
||||
await n8n.workflows.cards.getWorkflow('Workflow W1').click();
|
||||
|
||||
await n8n.canvas.addNode('Airtable', { action: 'Create a record' });
|
||||
await expect(n8n.ndv.getCredentialSelect()).toHaveValue('Credential C2');
|
||||
await n8n.ndv.clickBackToCanvasButton();
|
||||
await n8n.canvas.saveWorkflow();
|
||||
|
||||
await n8n.canvas.openNode('Append a block');
|
||||
|
||||
// C1 is shown but disabled (U3 doesn't own it)
|
||||
await expect(n8n.ndv.getNodeCredentialsSelect()).toHaveValue('Credential C1');
|
||||
await expect(n8n.ndv.getNodeCredentialsSelect()).toBeDisabled();
|
||||
|
||||
await n8n.ndv.clickBackToCanvasButton();
|
||||
});
|
||||
|
||||
test('should open W1, add node using C2 as U2', async ({ n8n }) => {
|
||||
await n8n.api.signin('member', 0);
|
||||
|
||||
await n8n.navigate.toWorkflows();
|
||||
|
||||
// U2 sees W1 and W2 (both owned by U2)
|
||||
await expect(n8n.workflows.cards.getWorkflows()).toHaveCount(2);
|
||||
await expect(n8n.workflows.cards.getWorkflow('Workflow W1')).toBeVisible();
|
||||
await expect(n8n.workflows.cards.getWorkflow('Workflow W2')).toBeVisible();
|
||||
|
||||
await n8n.workflows.cards.getWorkflow('Workflow W1').click();
|
||||
|
||||
await n8n.canvas.addNode('Airtable', { action: 'Create a record' });
|
||||
await expect(n8n.ndv.getCredentialSelect()).toHaveValue('Credential C2');
|
||||
await n8n.ndv.clickBackToCanvasButton();
|
||||
await n8n.canvas.saveWorkflow();
|
||||
|
||||
await n8n.canvas.openNode('Append a block');
|
||||
|
||||
// C1 is enabled (U2 owns it)
|
||||
await expect(n8n.ndv.getNodeCredentialsSelect().locator('input')).toHaveValue(
|
||||
'Credential C1',
|
||||
);
|
||||
await expect(n8n.ndv.getNodeCredentialsSelect().locator('input')).toBeEnabled();
|
||||
|
||||
await n8n.ndv.clickBackToCanvasButton();
|
||||
});
|
||||
|
||||
test('should not have access to W2, as U3', async ({ n8n }) => {
|
||||
const w2 = await n8n.workflowComposer.getWorkflowByName('Workflow W2');
|
||||
|
||||
await n8n.api.signin('member', 1);
|
||||
|
||||
await n8n.page.goto(`/workflow/${w2.id}`);
|
||||
|
||||
await expect(n8n.page).toHaveURL('/entity-not-authorized/workflow');
|
||||
});
|
||||
|
||||
test('should have access to W1, W2, as U1', async ({ n8n }) => {
|
||||
await n8n.api.signin('owner');
|
||||
|
||||
await n8n.navigate.toWorkflows();
|
||||
|
||||
// Owner sees W1 and W2 (created by U2)
|
||||
await expect(n8n.workflows.cards.getWorkflows()).toHaveCount(2);
|
||||
await expect(n8n.workflows.cards.getWorkflow('Workflow W1')).toBeVisible();
|
||||
await expect(n8n.workflows.cards.getWorkflow('Workflow W2')).toBeVisible();
|
||||
|
||||
await n8n.workflows.cards.getWorkflow('Workflow W1').click();
|
||||
|
||||
// C1 is enabled for owner
|
||||
await n8n.canvas.openNode('Append a block');
|
||||
await expect(n8n.ndv.getNodeCredentialsSelect().locator('input')).toHaveValue(
|
||||
'Credential C1',
|
||||
);
|
||||
await expect(n8n.ndv.getNodeCredentialsSelect().locator('input')).toBeEnabled();
|
||||
await n8n.ndv.clickBackToCanvasButton();
|
||||
|
||||
await n8n.navigate.toWorkflows();
|
||||
|
||||
await n8n.workflows.cards.getWorkflow('Workflow W2').click();
|
||||
await n8n.canvas.clickExecuteWorkflowButton();
|
||||
});
|
||||
|
||||
test('should automatically test C2 when opened by U2 sharee', async ({ n8n }) => {
|
||||
await n8n.api.signin('member', 0);
|
||||
|
||||
await n8n.navigate.toCredentials();
|
||||
|
||||
await n8n.credentials.cards.getCredential('Credential C2').click();
|
||||
|
||||
await expect(n8n.credentials.credentialModal.getTestSuccessTag()).toBeVisible();
|
||||
});
|
||||
|
||||
test('should work for admin role on credentials created by others', async ({ n8n }) => {
|
||||
await n8n.api.signin('member', 0);
|
||||
await n8n.navigate.toCredentials();
|
||||
await n8n.credentials.addResource.credential();
|
||||
await n8n.credentials.selectCredentialType('Notion API');
|
||||
await n8n.credentials.credentialModal.fillField('apiKey', TEST_API_KEY);
|
||||
await n8n.credentials.credentialModal.renameCredential('Credential C3');
|
||||
await n8n.credentials.credentialModal.save();
|
||||
await n8n.credentials.credentialModal.close();
|
||||
|
||||
await n8n.api.signin('admin');
|
||||
await n8n.navigate.toCredentials();
|
||||
await n8n.credentials.cards.getCredential('Credential C3').click();
|
||||
|
||||
await expect(n8n.credentials.credentialModal.getTestSuccessTag()).toBeVisible();
|
||||
|
||||
// Admin cannot see sensitive data (masked)
|
||||
const passwordInput = n8n.credentials.credentialModal
|
||||
.getCredentialInputs()
|
||||
.locator('input')
|
||||
.first();
|
||||
const inputValue = await passwordInput.inputValue();
|
||||
expect(inputValue).toContain('__n8n_BLANK_VALUE_');
|
||||
|
||||
await n8n.credentials.credentialModal.changeTab('Sharing');
|
||||
|
||||
await expect(
|
||||
n8n.credentials.credentialModal
|
||||
.getModal()
|
||||
.getByText('Sharing a credential allows people to use it in their workflows'),
|
||||
).toBeVisible();
|
||||
|
||||
await n8n.credentials.credentialModal.getUsersSelect().click();
|
||||
|
||||
await expect(
|
||||
n8n.credentials.credentialModal.getVisibleDropdown().getByTestId('project-sharing-info'),
|
||||
).toHaveCount(3);
|
||||
|
||||
// Admin can share with self
|
||||
await expect(
|
||||
n8n.credentials.credentialModal.getVisibleDropdown().getByText('admin@n8n.io'),
|
||||
).toBeVisible();
|
||||
|
||||
await n8n.credentials.credentialModal.getVisibleDropdown().getByText(OWNER_EMAIL).click();
|
||||
|
||||
await n8n.credentials.credentialModal.addUserToSharing(MEMBER_1_EMAIL);
|
||||
await n8n.credentials.credentialModal.addUserToSharing(ADMIN_EMAIL);
|
||||
|
||||
await n8n.credentials.credentialModal.saveSharing();
|
||||
await n8n.credentials.credentialModal.close();
|
||||
});
|
||||
|
||||
test('credentials should work between team and personal projects', async ({ n8n, api }) => {
|
||||
await api.resetDatabase();
|
||||
|
||||
await api.enableFeature('sharing');
|
||||
await api.enableFeature('advancedPermissions');
|
||||
await api.enableFeature('projectRole:admin');
|
||||
await api.enableFeature('projectRole:editor');
|
||||
await api.setMaxTeamProjectsQuota(-1);
|
||||
|
||||
await n8n.api.signin('owner');
|
||||
await n8n.navigate.toHome();
|
||||
|
||||
await n8n.projectComposer.createProject('Development');
|
||||
|
||||
await n8n.sideBar.clickHomeButton();
|
||||
await n8n.workflows.clickNewWorkflowCard();
|
||||
await n8n.canvas.importWorkflow('Test_workflow_1.json', 'Test workflow');
|
||||
|
||||
await n8n.sideBar.clickHomeButton();
|
||||
await n8n.projectTabs.clickCredentialsTab();
|
||||
await n8n.credentials.emptyListCreateCredentialButton.click();
|
||||
await n8n.credentials.selectCredentialType('Notion API');
|
||||
await n8n.credentials.credentialModal.fillField('apiKey', TEST_API_KEY);
|
||||
await n8n.credentials.credentialModal.renameCredential('Notion API');
|
||||
await n8n.credentials.credentialModal.save();
|
||||
await n8n.credentials.credentialModal.close();
|
||||
|
||||
await n8n.credentials.cards.getCredential('Notion API').click();
|
||||
await n8n.credentials.credentialModal.changeTab('Sharing');
|
||||
await n8n.credentials.credentialModal.getUsersSelect().click();
|
||||
|
||||
const sharingDropdown = n8n.credentials.credentialModal.getVisibleDropdown();
|
||||
await expect(sharingDropdown.locator('li')).toHaveCount(4);
|
||||
await expect(sharingDropdown.getByText('Development')).toBeVisible();
|
||||
|
||||
await sharingDropdown.getByText('Development').click();
|
||||
await n8n.credentials.credentialModal.saveSharing();
|
||||
await n8n.credentials.credentialModal.close();
|
||||
|
||||
await n8n.projectTabs.clickWorkflowsTab();
|
||||
await n8n.workflows.shareWorkflow('Test workflow');
|
||||
|
||||
await n8n.workflowSharingModal.getUsersSelect().click();
|
||||
const workflowSharingDropdown = n8n.workflowSharingModal.getVisibleDropdown();
|
||||
await expect(workflowSharingDropdown.locator('li')).toHaveCount(3);
|
||||
|
||||
await workflowSharingDropdown.locator('li').first().click();
|
||||
await n8n.workflowSharingModal.save();
|
||||
|
||||
await n8n.sideBar.getProjectMenuItems().first().click();
|
||||
await n8n.workflows.clickNewWorkflowCard();
|
||||
await n8n.canvas.importWorkflow('Test_workflow_1.json', 'Test workflow 2');
|
||||
|
||||
// Team project workflow cannot be shared
|
||||
await n8n.canvas.openShareModal();
|
||||
await expect(n8n.workflowSharingModal.getUsersSelect()).toHaveCount(0);
|
||||
|
||||
await n8n.workflowSharingModal.close();
|
||||
|
||||
await n8n.sideBar.getProjectMenuItems().first().click();
|
||||
await n8n.projectTabs.clickCredentialsTab();
|
||||
await n8n.credentials.addResource.credential();
|
||||
await n8n.credentials.selectCredentialType('Notion API');
|
||||
await n8n.credentials.credentialModal.fillField('apiKey', TEST_API_KEY);
|
||||
await n8n.credentials.credentialModal.renameCredential('Notion API 2');
|
||||
await n8n.credentials.credentialModal.save();
|
||||
|
||||
await n8n.credentials.credentialModal.changeTab('Sharing');
|
||||
await n8n.credentials.credentialModal.getUsersSelect().click();
|
||||
|
||||
const sharingDropdown2 = n8n.credentials.credentialModal.getVisibleDropdown();
|
||||
await expect(sharingDropdown2.locator('li')).toHaveCount(4);
|
||||
|
||||
await sharingDropdown2.locator('li').first().click();
|
||||
await n8n.credentials.credentialModal.saveSharing();
|
||||
await n8n.credentials.credentialModal.close();
|
||||
|
||||
// One credential labeled "Personal"
|
||||
await expect(n8n.credentials.cards.getCredentials()).toHaveCount(2);
|
||||
await expect(
|
||||
n8n.credentials.cards.getCredentials().filter({ hasText: 'Personal' }),
|
||||
).toHaveCount(1);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Credential Usage in Cross Shared Workflows', () => {
|
||||
test.beforeEach(async ({ n8n, api }) => {
|
||||
await api.resetDatabase();
|
||||
await api.enableFeature('sharing');
|
||||
await api.enableFeature('advancedPermissions');
|
||||
await api.enableFeature('projectRole:admin');
|
||||
await api.enableFeature('projectRole:editor');
|
||||
await api.setMaxTeamProjectsQuota(-1);
|
||||
|
||||
await n8n.api.signin('owner');
|
||||
await n8n.navigate.toCredentials();
|
||||
});
|
||||
|
||||
test('should only show credentials from the same team project', async ({ n8n }) => {
|
||||
await n8n.credentialsComposer.createFromList('Notion API', { apiKey: 'test' });
|
||||
|
||||
const devProject = await n8n.projectComposer.createProject('Development');
|
||||
await n8n.projectTabs.clickCredentialsTab();
|
||||
await n8n.credentialsComposer.createFromList(
|
||||
'Notion API',
|
||||
{ apiKey: 'test' },
|
||||
{ projectId: devProject.projectId },
|
||||
);
|
||||
|
||||
const testProject = await n8n.projectComposer.createProject('Test');
|
||||
await n8n.projectTabs.clickCredentialsTab();
|
||||
await n8n.credentialsComposer.createFromList(
|
||||
'Notion API',
|
||||
{ apiKey: 'test' },
|
||||
{ projectId: testProject.projectId },
|
||||
);
|
||||
|
||||
await n8n.projectTabs.clickWorkflowsTab();
|
||||
await n8n.workflows.clickNewWorkflowCard();
|
||||
|
||||
await n8n.canvas.addNode('Notion');
|
||||
await n8n.canvas.getFirstAction().click();
|
||||
|
||||
// Only Test project credential visible
|
||||
await n8n.ndv.getNodeCredentialsSelect().click();
|
||||
await expect(n8n.ndv.getVisiblePopper().locator('li')).toHaveCount(1);
|
||||
});
|
||||
|
||||
test('should only show credentials in their personal project for members', async ({ n8n }) => {
|
||||
await n8n.credentialsComposer.createFromList('Notion API', { apiKey: 'test' });
|
||||
|
||||
await n8n.navigate.toCredentials();
|
||||
await n8n.credentials.addResource.credential();
|
||||
await n8n.credentials.selectCredentialType('Notion API');
|
||||
await n8n.credentials.credentialModal.fillField('apiKey', 'test');
|
||||
await n8n.credentials.credentialModal.save();
|
||||
|
||||
await n8n.credentials.credentialModal.changeTab('Sharing');
|
||||
await n8n.credentials.credentialModal.addUserToSharing(MEMBER_0_EMAIL);
|
||||
await n8n.credentials.credentialModal.saveSharing();
|
||||
await n8n.credentials.credentialModal.close();
|
||||
|
||||
await n8n.api.signin('member', 0);
|
||||
await n8n.navigate.toCredentials();
|
||||
await n8n.credentialsComposer.createFromList('Notion API', { apiKey: 'test' });
|
||||
|
||||
await n8n.navigate.toWorkflows();
|
||||
await n8n.workflows.addResource.workflow();
|
||||
|
||||
await n8n.canvas.addNode('Notion');
|
||||
await n8n.canvas.getFirstAction().click();
|
||||
|
||||
// Own credential and shared credential visible
|
||||
await n8n.ndv.getNodeCredentialsSelect().click();
|
||||
await expect(n8n.ndv.getVisiblePopper().locator('li')).toHaveCount(2);
|
||||
});
|
||||
|
||||
test('should only show credentials in their personal project for members if the workflow was shared with them', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
const workflowName = 'Test workflow';
|
||||
|
||||
await n8n.credentialsComposer.createFromList('Notion API', { apiKey: 'test' });
|
||||
|
||||
await n8n.navigate.toWorkflows();
|
||||
await n8n.workflows.addResource.workflow();
|
||||
await n8n.canvas.setWorkflowName(workflowName);
|
||||
await n8n.page.keyboard.press('Enter');
|
||||
await n8n.canvas.openShareModal();
|
||||
await n8n.workflowSharingModal.addUser(MEMBER_0_EMAIL);
|
||||
await n8n.workflowSharingModal.save();
|
||||
|
||||
await n8n.api.signin('member', 0);
|
||||
await n8n.navigate.toCredentials();
|
||||
await n8n.credentialsComposer.createFromList('Notion API', { apiKey: 'test' });
|
||||
|
||||
await n8n.navigate.toWorkflows();
|
||||
await n8n.workflows.cards.getWorkflow(workflowName).click();
|
||||
|
||||
await n8n.canvas.addNode('Notion');
|
||||
await n8n.canvas.getFirstAction().click();
|
||||
|
||||
// Only own credential visible (not owner's)
|
||||
await n8n.ndv.getNodeCredentialsSelect().click();
|
||||
await expect(n8n.ndv.getVisiblePopper().locator('li')).toHaveCount(1);
|
||||
});
|
||||
|
||||
test("should show all credentials from all personal projects the workflow's been shared into for the global owner", async ({
|
||||
n8n,
|
||||
}) => {
|
||||
const workflowName = 'Test workflow';
|
||||
|
||||
await n8n.api.signin('member', 1);
|
||||
await n8n.navigate.toCredentials();
|
||||
await n8n.credentials.addResource.credential();
|
||||
await n8n.credentials.selectCredentialType('Notion API');
|
||||
await n8n.credentials.credentialModal.fillField('apiKey', 'test');
|
||||
await n8n.credentials.credentialModal.save();
|
||||
await n8n.credentials.credentialModal.close();
|
||||
|
||||
await n8n.api.signin('admin');
|
||||
await n8n.navigate.toCredentials();
|
||||
await n8n.credentials.addResource.credential();
|
||||
await n8n.credentials.selectCredentialType('Notion API');
|
||||
await n8n.credentials.credentialModal.fillField('apiKey', 'test');
|
||||
await n8n.credentials.credentialModal.save();
|
||||
await n8n.credentials.credentialModal.close();
|
||||
|
||||
await n8n.api.signin('member', 0);
|
||||
await n8n.navigate.toCredentials();
|
||||
await n8n.credentialsComposer.createFromList('Notion API', { apiKey: 'test' });
|
||||
await n8n.navigate.toWorkflows();
|
||||
await n8n.workflows.addResource.workflow();
|
||||
await n8n.canvas.setWorkflowName(workflowName);
|
||||
await n8n.page.keyboard.press('Enter');
|
||||
await n8n.canvas.openShareModal();
|
||||
await n8n.workflowSharingModal.addUser(OWNER_EMAIL);
|
||||
await n8n.workflowSharingModal.addUser(ADMIN_EMAIL);
|
||||
await n8n.workflowSharingModal.save();
|
||||
|
||||
await n8n.api.signin('owner');
|
||||
await n8n.navigate.toCredentials();
|
||||
await n8n.credentialsComposer.createFromList('Notion API', { apiKey: 'test' });
|
||||
await n8n.navigate.toWorkflows();
|
||||
await n8n.workflows.cards.getWorkflow(workflowName).click();
|
||||
|
||||
await n8n.canvas.addNode('Notion');
|
||||
await n8n.canvas.getFirstAction().click();
|
||||
|
||||
// Owner sees 3 credentials: admin's, U2's, owner's
|
||||
await n8n.ndv.getNodeCredentialsSelect().click();
|
||||
await expect(n8n.ndv.getVisiblePopper().locator('li')).toHaveCount(3);
|
||||
});
|
||||
|
||||
test('should show all personal credentials if the global owner owns the workflow', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
await n8n.api.signin('member', 0);
|
||||
await n8n.navigate.toCredentials();
|
||||
await n8n.credentials.addResource.credential();
|
||||
await n8n.credentials.selectCredentialType('Notion API');
|
||||
await n8n.credentials.credentialModal.fillField('apiKey', 'test');
|
||||
await n8n.credentials.credentialModal.save();
|
||||
await n8n.credentials.credentialModal.close();
|
||||
|
||||
await n8n.api.signin('owner');
|
||||
await n8n.navigate.toWorkflows();
|
||||
await n8n.workflows.addResource.workflow();
|
||||
await n8n.canvas.addNode('Notion');
|
||||
await n8n.canvas.getFirstAction().click();
|
||||
|
||||
// Owner sees member's credential (global owner privilege)
|
||||
await n8n.ndv.getNodeCredentialsSelect().click();
|
||||
await expect(n8n.ndv.getVisiblePopper().locator('li')).toHaveCount(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user