Skip to content

Commit

Permalink
Merge branch 'master' into refactor/binary_display_vue3
Browse files Browse the repository at this point in the history
  • Loading branch information
OlegIvaniv committed Jun 18, 2024
2 parents 7c4d746 + 1e8716a commit 10dc8d8
Show file tree
Hide file tree
Showing 84 changed files with 3,556 additions and 1,975 deletions.
9 changes: 9 additions & 0 deletions cypress/composables/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const getProjectTabs = () => cy.getByTestId('project-tabs').find('a');
export const getProjectTabWorkflows = () => getProjectTabs().filter('a[href$="/workflows"]');
export const getProjectTabCredentials = () => getProjectTabs().filter('a[href$="/credentials"]');
export const getProjectTabSettings = () => getProjectTabs().filter('a[href$="/settings"]');
export const getProjectSettingsNameInput = () => cy.getByTestId('project-settings-name-input');
export const getProjectSettingsSaveButton = () => cy.getByTestId('project-settings-save-button');
export const getProjectSettingsCancelButton = () =>
cy.getByTestId('project-settings-cancel-button');
Expand Down Expand Up @@ -55,3 +56,11 @@ export function createCredential(name: string) {
credentialsModal.actions.save();
credentialsModal.actions.close();
}

export const actions = {
createProject: (name: string) => {
getAddProjectButton().click();
getProjectSettingsNameInput().type(name);
getProjectSettingsSaveButton().click();
},
};
177 changes: 176 additions & 1 deletion cypress/e2e/17-sharing.cy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { INSTANCE_MEMBERS, INSTANCE_OWNER, INSTANCE_ADMIN } from '../constants';
import { INSTANCE_MEMBERS, INSTANCE_OWNER, INSTANCE_ADMIN, NOTION_NODE_NAME } from '../constants';
import {
CredentialsModal,
CredentialsPage,
Expand All @@ -8,6 +8,7 @@ import {
WorkflowsPage,
} from '../pages';
import { getVisibleSelect } from '../utils';
import * as projects from '../composables/projects';

/**
* User U1 - Instance owner
Expand Down Expand Up @@ -188,3 +189,177 @@ describe('Sharing', { disableAutoLogin: true }, () => {
credentialsModal.actions.close();
});
});

describe('Credential Usage in Cross Shared Workflows', () => {
beforeEach(() => {
cy.resetDatabase();
cy.enableFeature('advancedPermissions');
cy.enableFeature('projectRole:admin');
cy.enableFeature('projectRole:editor');
cy.changeQuota('maxTeamProjects', -1);
cy.reload();
cy.signinAsOwner();
cy.visit(credentialsPage.url);
});

it('should only show credentials from the same team project', () => {
cy.enableFeature('advancedPermissions');
cy.enableFeature('projectRole:admin');
cy.enableFeature('projectRole:editor');
cy.changeQuota('maxTeamProjects', -1);

// 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.actions.createProject('Development');
projects.getProjectTabCredentials().click();
credentialsPage.getters.emptyListCreateCredentialButton().click();
credentialsModal.actions.createNewCredential('Notion API');

// Create a notion credential in another project
projects.actions.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 (+ the 'Create new' option) 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', () => {
cy.enableFeature('sharing');
cy.reload();

// 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();
cy.visit(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 (+ the 'Create new' option)
// should be in the dropdown
workflowPage.getters.nodeCredentialsSelect().click();
getVisibleSelect().find('li').should('have.length', 3);
});

it('should only show credentials in their personal project for members if the workflow was shared with them', () => {
const workflowName = 'Test workflow';
cy.enableFeature('sharing');
cy.reload();

// 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();
cy.visit(credentialsPage.url);
credentialsPage.getters.emptyListCreateCredentialButton().click();
credentialsModal.actions.createNewCredential('Notion API');
cy.visit(workflowsPage.url);
workflowsPage.getters.workflowCard(workflowName).click();
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true);

// Only the own credential the shared one (+ the 'Create new' option)
// should be in the dropdown
workflowPage.getters.nodeCredentialsSelect().click();
getVisibleSelect().find('li').should('have.length', 2);
});

it("should show all credentials from all personal projects the workflow's been shared into for the global owner", () => {
const workflowName = 'Test workflow';
cy.enableFeature('sharing');

// As member 1, create a new notion credential. This should not show up.
cy.signinAsMember(1);
cy.visit(credentialsPage.url);
credentialsPage.getters.emptyListCreateCredentialButton().click();
credentialsModal.actions.createNewCredential('Notion API');

// As admin, create a new notion credential. This should show up.
cy.signinAsAdmin();
cy.visit(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();
cy.visit(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();
cy.visit(credentialsPage.url);
credentialsPage.getters.createCredentialButton().click();
credentialsModal.actions.createNewCredential('Notion API');
cy.visit(workflowsPage.url);
workflowsPage.getters.workflowCard(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', 4);
});

it('should show all personal credentials if the global owner owns the workflow', () => {
cy.enableFeature('sharing');

// As member 0, create a new notion credential.
cy.signinAsMember();
cy.visit(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', 2);
});
});
4 changes: 2 additions & 2 deletions cypress/e2e/30-editor-after-route-changes.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ const switchBetweenEditorAndHistory = () => {

const switchBetweenEditorAndWorkflowlist = () => {
cy.getByTestId('menu-item').first().click();
cy.wait(['@getUsers', '@getWorkflows', '@getActiveWorkflows', '@getCredentials']);
cy.wait(['@getUsers', '@getWorkflows', '@getActiveWorkflows', '@getProjects']);

cy.getByTestId('resources-list-item').first().click();

Expand Down Expand Up @@ -197,7 +197,7 @@ describe('Editor zoom should work after route changes', () => {
cy.intercept('GET', '/rest/users').as('getUsers');
cy.intercept('GET', '/rest/workflows?*').as('getWorkflows');
cy.intercept('GET', '/rest/active-workflows').as('getActiveWorkflows');
cy.intercept('GET', '/rest/credentials?*').as('getCredentials');
cy.intercept('GET', '/rest/projects').as('getProjects');

switchBetweenEditorAndHistory();
zoomInAndCheckNodes();
Expand Down
46 changes: 46 additions & 0 deletions cypress/e2e/43-oauth-flow.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { CredentialsPage, CredentialsModal } from '../pages';

const credentialsPage = new CredentialsPage();
const credentialsModal = new CredentialsModal();

describe('Credentials', () => {
it('create and connect with Google OAuth2', () => {
// Open credentials page
cy.visit(credentialsPage.url, {
onBeforeLoad(win) {
cy.stub(win, 'open').as('windowOpen');
},
});

// Add a new Google OAuth2 credential
credentialsPage.getters.emptyListCreateCredentialButton().click();
credentialsModal.getters.newCredentialTypeOption('Google OAuth2 API').click();
credentialsModal.getters.newCredentialTypeButton().click();

// Fill in the key/secret and save
credentialsModal.actions.fillField('clientId', 'test-key');
credentialsModal.actions.fillField('clientSecret', 'test-secret');
credentialsModal.actions.save();

// Connect to Google
credentialsModal.getters.oauthConnectButton().click();
cy.get('@windowOpen').should(
'have.been.calledOnceWith',
Cypress.sinon.match(
'https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&prompt=consent&client_id=test-key&redirect_uri=http%3A%2F%2Flocalhost%3A5678%2Frest%2Foauth2-credential%2Fcallback&response_type=code',
),
'OAuth Authorization',
'scrollbars=no,resizable=yes,status=no,titlebar=noe,location=no,toolbar=no,menubar=no,width=500,height=700',
);

// Emulate successful save using BroadcastChannel
cy.window().then(() => {
const channel = new BroadcastChannel('oauth-callback');
channel.postMessage('success');
});

// Check that the credential was saved and connected successfully
credentialsModal.getters.saveButton().should('contain.text', 'Saved');
credentialsModal.getters.oauthConnectSuccessBanner().should('be.visible');
});
});
27 changes: 22 additions & 5 deletions cypress/pages/modals/credentials-modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,16 @@ export class CredentialsModal extends BasePage {
newCredentialTypeOption: (credentialType: string) =>
cy.getByTestId('new-credential-type-select-option').contains(credentialType),
newCredentialTypeButton: () => cy.getByTestId('new-credential-type-button'),
connectionParameters: () => cy.getByTestId('credential-connection-parameter'),
connectionParameter: (fieldName: string) =>
this.getters.connectionParameters().find(`:contains('${fieldName}') .n8n-input input`),
this.getters.credentialInputs().find(`:contains('${fieldName}') .n8n-input input`),
name: () => cy.getByTestId('credential-name'),
nameInput: () => cy.getByTestId('credential-name').find('input'),
// Saving of the credentials takes a while on the CI so we need to increase the timeout
saveButton: () => cy.getByTestId('credential-save-button', { timeout: 5000 }),
deleteButton: () => cy.getByTestId('credential-delete-button'),
closeButton: () => this.getters.editCredentialModal().find('.el-dialog__close').first(),
oauthConnectButton: () => cy.getByTestId('oauth-connect-button'),
oauthConnectSuccessBanner: () => cy.getByTestId('oauth-connect-success-banner'),
credentialsEditModal: () => cy.getByTestId('credential-edit-dialog'),
credentialsAuthTypeSelector: () => cy.getByTestId('node-auth-type-selector'),
credentialAuthTypeRadioButtons: () =>
Expand Down Expand Up @@ -55,7 +56,7 @@ export class CredentialsModal extends BasePage {
close: () => {
this.getters.closeButton().click();
},
fillCredentialsForm: () => {
fillCredentialsForm: (closeModal = true) => {
this.getters.credentialsEditModal().should('be.visible');
this.getters.credentialInputs().should('have.length.greaterThan', 0);
this.getters
Expand All @@ -65,14 +66,30 @@ export class CredentialsModal extends BasePage {
cy.wrap($el).type('test');
});
this.getters.saveButton().click();
this.getters.closeButton().click();
if (closeModal) {
this.getters.closeButton().click();
}
},
fillField: (fieldName: string, value: string) => {
this.getters
.credentialInputs()
.getByTestId(`parameter-input-${fieldName}`)
.find('input')
.type(value);
},
createNewCredential: (type: string, closeModal = true) => {
this.getters.newCredentialModal().should('be.visible');
this.getters.newCredentialTypeSelect().should('be.visible');
this.getters.newCredentialTypeOption(type).click();
this.getters.newCredentialTypeButton().click();
this.actions.fillCredentialsForm(closeModal);
},
renameCredential: (newName: string) => {
this.getters.nameInput().type('{selectall}');
this.getters.nameInput().type(newName);
this.getters.nameInput().type('{enter}');
},
changeTab: (tabName: string) => {
changeTab: (tabName: 'Sharing') => {
this.getters.menuItem(tabName).click();
},
};
Expand Down
5 changes: 4 additions & 1 deletion cypress/support/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,13 @@ declare global {
* @param [workflowName] Optional name for the workflow. A random nanoid is used if not given
*/
createFixtureWorkflow(fixtureKey: string, workflowName?: string): void;
/** @deprecated */
/** @deprecated use signinAsOwner, signinAsAdmin or signinAsMember instead */
signin(payload: SigninPayload): void;
signinAsOwner(): void;
signinAsAdmin(): void;
/**
* Omitting the index will default to index 0.
*/
signinAsMember(index?: number): void;
signout(): void;
overrideSettings(value: Partial<IN8nUISettings>): void;
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"typescript": "*",
"vite": "^5.2.12",
"vitest": "^1.6.0",
"vitest-mock-extended": "^1.3.1",
"vue-tsc": "^2.0.19"
},
"pnpm": {
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ export interface IExecutionResponse extends IExecutionBase {
retryOf?: string;
retrySuccessId?: string;
workflowData: IWorkflowBase | WorkflowWithSharingsAndCredentials;
customData: Record<string, string>;
}

// Flatted data to save memory when saving in database or transferring
Expand All @@ -158,6 +159,7 @@ export interface IExecutionFlattedDb extends IExecutionBase {
id: string;
data: string;
workflowData: Omit<IWorkflowBase, 'pinData'>;
customData: Record<string, string>;
}

export interface IExecutionFlattedResponse extends IExecutionFlatted {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,5 @@ properties:
type: string
nullable: true
format: date-time
customData:
type: object
Loading

0 comments on commit 10dc8d8

Please sign in to comment.