Skip to content

Commit

Permalink
Merge branch 'master' of https://github.com/n8n-io/n8n into node-628-…
Browse files Browse the repository at this point in the history
…google-sheets-add-by-name-option-to-the-sheet-selector-not
  • Loading branch information
michael-radency committed Jan 10, 2024
2 parents 9813688 + b31fcdb commit 5d5ae5e
Show file tree
Hide file tree
Showing 315 changed files with 7,135 additions and 3,194 deletions.
9 changes: 6 additions & 3 deletions .github/scripts/package.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
{
"dependencies": {
"cacheable-lookup": "6.1.0",
"conventional-changelog": "^4.0.0",
"glob": "^10.3.0",
"semver": "^7.5.4",
"tempfile": "^5.0.0",
"debug": "4.3.4",
"glob": "10.3.10",
"p-limit": "3.1.0",
"semver": "7.5.4",
"tempfile": "5.0.0",
"typescript": "*"
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
#!/usr/bin/env node

const packages = ['nodes-base', '@n8n/nodes-langchain'];
const concurrency = 20;
let exitCode = 0;

const debug = require('debug')('n8n');
const path = require('path');
const https = require('https');
const glob = require('fast-glob');
const glob = require('glob');
const pLimit = require('p-limit');
const Lookup = require('cacheable-lookup').default;

const nodesBaseDir = path.resolve(__dirname, '../packages/nodes-base');
const agent = new https.Agent({ keepAlive: true, keepAliveMsecs: 5000 });
new Lookup().install(agent);
const limiter = pLimit(concurrency);

const validateUrl = async (kind, name, documentationUrl) =>
new Promise((resolve, reject) => {
Expand All @@ -22,21 +30,26 @@ const validateUrl = async (kind, name, documentationUrl) =>
port: 443,
path: url.pathname,
method: 'HEAD',
agent,
},
(res) => {
debug('✓', kind, name);
resolve([name, res.statusCode]);
},
(res) => resolve([name, res.statusCode]),
)
.on('error', (e) => reject(e))
.end();
});

const checkLinks = async (kind) => {
let types = require(path.join(nodesBaseDir, `dist/types/${kind}.json`));
const checkLinks = async (baseDir, kind) => {
let types = require(path.join(baseDir, `dist/types/${kind}.json`));
if (kind === 'nodes')
types = types.filter(({ codex }) => !!codex?.resources?.primaryDocumentation);
const limit = pLimit(30);
debug(kind, types.length);

const statuses = await Promise.all(
types.map((type) =>
limit(() => {
limiter(() => {
const documentationUrl =
kind === 'credentials'
? type.documentationUrl
Expand All @@ -55,10 +68,13 @@ const checkLinks = async (kind) => {

if (missingDocs.length) console.log('Documentation URL missing for %s', kind, missingDocs);
if (invalidUrls.length) console.log('Documentation URL invalid for %s', kind, invalidUrls);
if (missingDocs.length || invalidUrls.length) process.exit(1);
if (missingDocs.length || invalidUrls.length) exitCode = 1;
};

(async () => {
await checkLinks('credentials');
await checkLinks('nodes');
for (const packageName of packages) {
const baseDir = path.resolve(__dirname, '../../packages', packageName);
await Promise.all([checkLinks(baseDir, 'credentials'), checkLinks(baseDir, 'nodes')]);
if (exitCode !== 0) process.exit(exitCode);
}
})();
6 changes: 4 additions & 2 deletions .github/workflows/check-documentation-urls.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@ jobs:
run: pnpm install --frozen-lockfile

- name: Build nodes-base
run: pnpm --filter @n8n/client-oauth2 --filter n8n-workflow --filter n8n-core --filter n8n-nodes-base build
run: pnpm --filter @n8n/client-oauth2 --filter n8n-workflow --filter n8n-core --filter n8n-nodes-base --filter @n8n/n8n-nodes-langchain build

- run: npm install --prefix=.github/scripts --no-package-lock

- name: Test URLs
run: node scripts/validate-docs-links.js
run: node .github/scripts/validate-docs-links.js

- name: Notify Slack on failure
uses: act10ns/[email protected]
Expand Down
7 changes: 6 additions & 1 deletion .github/workflows/docker-images-nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ on:
description: 'URL to call after Docker Image got built successfully.'
required: false
default: ''
include-arm64:
description: 'Include ARM64 support'
type: boolean
required: true
default: false

jobs:
build:
Expand Down Expand Up @@ -76,7 +81,7 @@ jobs:
build-args: |
N8N_RELEASE_TYPE=nightly
file: ./docker/images/n8n-custom/Dockerfile
platforms: linux/amd64
platforms: ${{ github.event.inputs.include-arm64 == 'true' && 'linux/amd64,linux/arm64' || 'linux/amd64' }}
provenance: false
push: true
tags: ${{ secrets.DOCKER_USERNAME }}/n8n:${{ github.event.inputs.tag || 'nightly' }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/e2e-reusable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ on:
containers:
description: 'Number of containers to run tests in.'
required: false
default: '[1, 2, 3, 4, 5, 6, 7, 8]'
default: '[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]'
type: string
pr_number:
description: 'PR number to run tests for.'
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/units-tests-reusable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,4 @@ jobs:
if: ${{ inputs.collectCoverage == 'true' }}
uses: codecov/codecov-action@v3
with:
files: packages/@n8n/client-oauth2/coverage/cobertura-coverage.xml,packages/cli/coverage/cobertura-coverage.xml,packages/core/coverage/cobertura-coverage.xml,packages/design-system/coverage/cobertura-coverage.xml,packages/editor-ui/coverage/cobertura-coverage.xml,packages/nodes-base/coverage/cobertura-coverage.xml,packages/workflow/coverage/cobertura-coverage.xml
files: packages/@n8n/chat/coverage/cobertura-coverage.xml,packages/@n8n/nodes-langchain/coverage/cobertura-coverage.xml,packages/@n8n/permissions/coverage/cobertura-coverage.xml,packages/@n8n/client-oauth2/coverage/cobertura-coverage.xml,packages/cli/coverage/cobertura-coverage.xml,packages/core/coverage/cobertura-coverage.xml,packages/design-system/coverage/cobertura-coverage.xml,packages/editor-ui/coverage/cobertura-coverage.xml,packages/nodes-base/coverage/cobertura-coverage.xml,packages/workflow/coverage/cobertura-coverage.xml
13 changes: 13 additions & 0 deletions cypress/composables/modals/workflow-credential-setup-modal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Getters
*/

export const getWorkflowCredentialsModal = () => cy.getByTestId('setup-workflow-credentials-modal');

export const getContinueButton = () => cy.getByTestId('continue-button');

/**
* Actions
*/

export const closeModalFromContinueButton = () => getContinueButton().click();
14 changes: 14 additions & 0 deletions cypress/composables/setup-template-form-step.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* Getters
*/

export const getFormStep = () => cy.getByTestId('setup-credentials-form-step');

export const getStepHeading = ($el: JQuery<HTMLElement>) =>
cy.wrap($el).findChildByTestId('credential-step-heading');

export const getStepDescription = ($el: JQuery<HTMLElement>) =>
cy.wrap($el).findChildByTestId('credential-step-description');

export const getCreateAppCredentialsButton = (appName: string) =>
cy.get(`button:contains("Create new ${appName} credential")`);
5 changes: 5 additions & 0 deletions cypress/composables/setup-workflow-credentials-button.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/**
* Getters
*/

export const getSetupWorkflowCredentialsButton = () => cy.get(`button:contains("Set up template")`);
4 changes: 2 additions & 2 deletions cypress/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ export const INSTANCE_MEMBERS = [
];

export const MANUAL_TRIGGER_NODE_NAME = 'Manual Trigger';
export const MANUAL_TRIGGER_NODE_DISPLAY_NAME = 'When clicking "Execute Workflow"';
export const MANUAL_CHAT_TRIGGER_NODE_NAME = 'Manual Chat Trigger';
export const MANUAL_TRIGGER_NODE_DISPLAY_NAME = 'When clicking "Test Workflow"';
export const MANUAL_CHAT_TRIGGER_NODE_NAME = 'Chat Trigger';
export const SCHEDULE_TRIGGER_NODE_NAME = 'Schedule Trigger';
export const CODE_NODE_NAME = 'Code';
export const SET_NODE_NAME = 'Set';
Expand Down
2 changes: 1 addition & 1 deletion cypress/e2e/13-pinning.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ describe('Data pinning', () => {
});

function setExpressionOnStringValueInSet(expression: string) {
cy.get('button').contains('Execute node').click();
cy.get('button').contains('Test step').click();
cy.get('.fixed-collection-parameter > :nth-child(2) > .button > span').click();

ndv.getters.nthParam(4).contains('Expression').invoke('show').click();
Expand Down
5 changes: 1 addition & 4 deletions cypress/e2e/16-form-trigger-node.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,7 @@ describe('n8n Form Trigger', () => {

it("add node by clicking on 'On form submission'", () => {
workflowPage.getters.canvasPlusButton().click();
cy.get('#node-view-root > div:nth-child(2) > div > div > aside ')
.find('span')
.contains('On form submission')
.click();
workflowPage.getters.nodeCreatorNodeItems().contains('On form submission').click();
ndv.getters.parameterInput('formTitle').type('Test Form');
ndv.getters.parameterInput('formDescription').type('Test Form Description');
ndv.getters.parameterInput('fieldLabel').type('Test Field 1');
Expand Down
4 changes: 4 additions & 0 deletions cypress/e2e/29-templates.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ describe('Templates', () => {

it('should save template id with the workflow', () => {
cy.visit(templatesPage.url);
cy.intercept('GET', '**/api/templates/**').as('loadApi');
cy.get('.el-skeleton.n8n-loading').should('not.exist');
templatesPage.getters.firstTemplateCard().should('exist');
cy.wait('@loadApi');
templatesPage.getters.firstTemplateCard().click();
cy.url().should('include', '/templates/');

Expand Down
110 changes: 84 additions & 26 deletions cypress/e2e/34-template-credentials-setup.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,29 @@ import {
import * as templateCredentialsSetupPage from '../pages/template-credential-setup';
import { TemplateWorkflowPage } from '../pages/template-workflow';
import { WorkflowPage } from '../pages/workflow';
import * as formStep from '../composables/setup-template-form-step';
import { getSetupWorkflowCredentialsButton } from '../composables/setup-workflow-credentials-button';
import * as setupCredsModal from '../composables/modals/workflow-credential-setup-modal';

const templateWorkflowPage = new TemplateWorkflowPage();
const workflowPage = new WorkflowPage();

const testTemplate = templateCredentialsSetupPage.testData.simpleTemplate;

// NodeView uses beforeunload listener that will show a browser
// native popup, which will block cypress from continuing / exiting.
// This prevent the registration of the listener.
Cypress.on('window:before:load', (win) => {
const origAddEventListener = win.addEventListener;
win.addEventListener = (eventName: string, listener: any, opts: any) => {
if (eventName === 'beforeunload') {
return;
}

return origAddEventListener.call(win, eventName, listener, opts);
};
});

describe('Template credentials setup', () => {
beforeEach(() => {
cy.intercept('GET', `https://api.n8n.io/api/templates/workflows/${testTemplate.id}`, {
Expand All @@ -26,7 +43,7 @@ describe('Template credentials setup', () => {
templateWorkflowPage.actions.clickUseThisWorkflowButton();

templateCredentialsSetupPage.getters
.title(`Setup 'Promote new Shopify products on Twitter and Telegram' template`)
.title(`Set up 'Promote new Shopify products on Twitter and Telegram' template`)
.should('be.visible');
});

Expand All @@ -36,23 +53,23 @@ describe('Template credentials setup', () => {
clickUseWorkflowButtonByTitle('Promote new Shopify products on Twitter and Telegram');

templateCredentialsSetupPage.getters
.title(`Setup 'Promote new Shopify products on Twitter and Telegram' template`)
.title(`Set up 'Promote new Shopify products on Twitter and Telegram' template`)
.should('be.visible');
});

it('can be opened with a direct url', () => {
templateCredentialsSetupPage.visitTemplateCredentialSetupPage(testTemplate.id);

templateCredentialsSetupPage.getters
.title(`Setup 'Promote new Shopify products on Twitter and Telegram' template`)
.title(`Set up 'Promote new Shopify products on Twitter and Telegram' template`)
.should('be.visible');
});

it('has all the elements on page', () => {
templateCredentialsSetupPage.visitTemplateCredentialSetupPage(testTemplate.id);

templateCredentialsSetupPage.getters
.title(`Setup 'Promote new Shopify products on Twitter and Telegram' template`)
.title(`Set up 'Promote new Shopify products on Twitter and Telegram' template`)
.should('be.visible');

templateCredentialsSetupPage.getters
Expand All @@ -69,13 +86,9 @@ describe('Template credentials setup', () => {
'The credential you select will be used in the Telegram node of the workflow template.',
];

templateCredentialsSetupPage.getters.appCredentialSteps().each(($el, index) => {
templateCredentialsSetupPage.getters
.stepHeading($el)
.should('have.text', expectedAppNames[index]);
templateCredentialsSetupPage.getters
.stepDescription($el)
.should('have.text', expectedAppDescriptions[index]);
formStep.getFormStep().each(($el, index) => {
formStep.getStepHeading($el).should('have.text', expectedAppNames[index]);
formStep.getStepDescription($el).should('have.text', expectedAppDescriptions[index]);
});
});

Expand All @@ -100,10 +113,7 @@ describe('Template credentials setup', () => {
templateCredentialsSetupPage.fillInDummyCredentialsForAppWithConfirm('X (Formerly Twitter)');
templateCredentialsSetupPage.fillInDummyCredentialsForApp('Telegram');

cy.intercept('POST', '/rest/workflows').as('createWorkflow');
templateCredentialsSetupPage.getters.continueButton().should('be.enabled');
templateCredentialsSetupPage.getters.continueButton().click();
cy.wait('@createWorkflow');
templateCredentialsSetupPage.finishCredentialSetup();

workflowPage.getters.canvasNodes().should('have.length', 3);

Expand Down Expand Up @@ -137,25 +147,73 @@ describe('Template credentials setup', () => {
'The credential you select will be used in the Nextcloud node of the workflow template.',
];

templateCredentialsSetupPage.getters.appCredentialSteps().each(($el, index) => {
templateCredentialsSetupPage.getters
.stepHeading($el)
.should('have.text', expectedAppNames[index]);
templateCredentialsSetupPage.getters
.stepDescription($el)
.should('have.text', expectedAppDescriptions[index]);
formStep.getFormStep().each(($el, index) => {
formStep.getStepHeading($el).should('have.text', expectedAppNames[index]);
formStep.getStepDescription($el).should('have.text', expectedAppDescriptions[index]);
});

templateCredentialsSetupPage.getters.continueButton().should('be.disabled');

templateCredentialsSetupPage.fillInDummyCredentialsForApp('Email (IMAP)');
templateCredentialsSetupPage.fillInDummyCredentialsForApp('Nextcloud');

cy.intercept('POST', '/rest/workflows').as('createWorkflow');
templateCredentialsSetupPage.getters.continueButton().should('be.enabled');
templateCredentialsSetupPage.getters.continueButton().click();
cy.wait('@createWorkflow');
templateCredentialsSetupPage.finishCredentialSetup();

workflowPage.getters.canvasNodes().should('have.length', 3);
});

describe('Credential setup from workflow editor', () => {
beforeEach(() => {
cy.resetDatabase();
cy.signinAsOwner();
});

it('should allow credential setup from workflow editor if user skips it during template setup', () => {
templateCredentialsSetupPage.visitTemplateCredentialSetupPage(testTemplate.id);
templateCredentialsSetupPage.getters.skipLink().click();

getSetupWorkflowCredentialsButton().should('be.visible');
});

it('should allow credential setup from workflow editor if user fills in credentials partially during template setup', () => {
templateCredentialsSetupPage.visitTemplateCredentialSetupPage(testTemplate.id);
templateCredentialsSetupPage.fillInDummyCredentialsForApp('Shopify');

templateCredentialsSetupPage.finishCredentialSetup();

getSetupWorkflowCredentialsButton().should('be.visible');
});

it('should fill credentials from workflow editor', () => {
templateCredentialsSetupPage.visitTemplateCredentialSetupPage(testTemplate.id);
templateCredentialsSetupPage.getters.skipLink().click();

getSetupWorkflowCredentialsButton().click();
setupCredsModal.getWorkflowCredentialsModal().should('be.visible');

templateCredentialsSetupPage.fillInDummyCredentialsForApp('Shopify');
templateCredentialsSetupPage.fillInDummyCredentialsForAppWithConfirm('X (Formerly Twitter)');
templateCredentialsSetupPage.fillInDummyCredentialsForApp('Telegram');

setupCredsModal.closeModalFromContinueButton();
setupCredsModal.getWorkflowCredentialsModal().should('not.exist');

// Focus the canvas so the copy to clipboard works
workflowPage.getters.canvasNodes().eq(0).realClick();
workflowPage.actions.selectAll();
workflowPage.actions.hitCopy();

cy.grantBrowserPermissions('clipboardReadWrite', 'clipboardSanitizedWrite');
// Check workflow JSON by copying it to clipboard
cy.readClipboard().then((workflowJSON) => {
const workflow = JSON.parse(workflowJSON);

workflow.nodes.forEach((node: any) => {
expect(Object.keys(node.credentials ?? {})).to.have.lengthOf(1);
});
});

getSetupWorkflowCredentialsButton().should('not.exist');
});
});
});
Loading

0 comments on commit 5d5ae5e

Please sign in to comment.