diff --git a/CHANGELOG.md b/CHANGELOG.md index 67704a9649cd6..38c765cb5e6fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,99 @@ +# [0.227.0](https://github.com/n8n-io/n8n/compare/n8n@0.226.0...n8n@0.227.0) (2023-05-03) + + +### Bug Fixes + +* **AWS S3 Node:** Fix File upload, and add node tests ([#6153](https://github.com/n8n-io/n8n/issues/6153)) ([deb4c04](https://github.com/n8n-io/n8n/commit/deb4c04f346f8e5985b5f6c3f3a3e929fde13e5b)) +* **Compression Node:** Fix issue with decompression failing with uppercase extensions ([#6098](https://github.com/n8n-io/n8n/issues/6098)) ([aa59329](https://github.com/n8n-io/n8n/commit/aa593298365eabd6eb1dda9fe3f06e7eae7c5ea9)) +* **core:** Account for nodes with renamable content ([#6109](https://github.com/n8n-io/n8n/issues/6109)) ([c99f158](https://github.com/n8n-io/n8n/commit/c99f158120b3c1ffca1718be337afc73d6ec9e65)) +* **core:** Assign Unknown Error only if message or description not present in error ([8aedc03](https://github.com/n8n-io/n8n/commit/8aedc03ddad3f83ffd2569be5b61710f27d2f672)) +* **core:** Avoid using `Object.keys` on Buffer and other non-plain objects ([#6131](https://github.com/n8n-io/n8n/issues/6131)) ([a3aba83](https://github.com/n8n-io/n8n/commit/a3aba835a15a8a32acc1e1ff0b972007df2b2b34)) +* **core:** Better error message in Webhook node when using the POST method ([a0dd17e](https://github.com/n8n-io/n8n/commit/a0dd17e1151e668b95dc57367a0b100d00913ea3)) +* **core:** Better errors for common status codes fix ([700cc39](https://github.com/n8n-io/n8n/commit/700cc39cbc7da3c70513ff586dc97319456308ae)) +* **core:** Fix `hasOwnProperty` on augmented objects ([#6124](https://github.com/n8n-io/n8n/issues/6124)) ([206b6b9](https://github.com/n8n-io/n8n/commit/206b6b90b860ceaab58b9bdd5ff20ffc741c13fa)) +* **core:** Fix bug running addUserActivatedColumn migration on MariaDB ([#6157](https://github.com/n8n-io/n8n/issues/6157)) ([570790e](https://github.com/n8n-io/n8n/commit/570790ed0c9521e09b6414bc1da2c596f17ff072)) +* **core:** Fix canceled execution status ([#6142](https://github.com/n8n-io/n8n/issues/6142)) ([839a56a](https://github.com/n8n-io/n8n/commit/839a56a682674baf44d5beececdbe677d18c0d89)) +* **core:** Improve saml endpoints and audit events ([#6107](https://github.com/n8n-io/n8n/issues/6107)) ([c0b1cdd](https://github.com/n8n-io/n8n/commit/c0b1cddc91fe199377c301f02f230827f231ba73)) +* **core:** Remove SAML config metadataUrl if XML metadata is set directly ([#6143](https://github.com/n8n-io/n8n/issues/6143)) ([25fe14b](https://github.com/n8n-io/n8n/commit/25fe14be56482477c00a360914730b25c9028443)) +* **core:** Skip auth for controllers/routes that don't use the `Authorized` decorator, or use `Authorized('none')` ([#6106](https://github.com/n8n-io/n8n/issues/6106)) ([59aee22](https://github.com/n8n-io/n8n/commit/59aee2270bdc0c8360aa534237b7f6015d382346)) +* Correctly allow sharees to test credential when opening the modal ([#6111](https://github.com/n8n-io/n8n/issues/6111)) ([2e73f4a](https://github.com/n8n-io/n8n/commit/2e73f4abd04ba7ab929b0fce57bf12651a0a2e49)) +* **Date & Time Node:** Numbers conversions fix ([14f7114](https://github.com/n8n-io/n8n/commit/14f71146e21026721fc9d5883bb9d73d38afcf8c)) +* **editor:** Change execution list tab loader design ([#6120](https://github.com/n8n-io/n8n/issues/6120)) ([188ef04](https://github.com/n8n-io/n8n/commit/188ef042cd58b9194dadef4cc68deb3510688c26)) +* **editor:** Disable changing of email and pw when SAML login enabled ([#6104](https://github.com/n8n-io/n8n/issues/6104)) ([3e9ecd9](https://github.com/n8n-io/n8n/commit/3e9ecd939742df8d8ced9179aaa26b081139befa)) +* **editor:** Fix `Show details` summary ([#6113](https://github.com/n8n-io/n8n/issues/6113)) ([90a62cc](https://github.com/n8n-io/n8n/commit/90a62ccfb5b4a959d72336d284ad4ac3b17af582)) +* **editor:** Fix copy selection behavior ([#6112](https://github.com/n8n-io/n8n/issues/6112)) ([1607aeb](https://github.com/n8n-io/n8n/commit/1607aeb9f94700793d58604ea4f89c5555d43981)) +* **editor:** Fix cropped off completions docstrings ([#6129](https://github.com/n8n-io/n8n/issues/6129)) ([85e8145](https://github.com/n8n-io/n8n/commit/85e8145439f89e76fe5fe3a659430c03738d6e2b)) +* **editor:** Fix focus jumping when using chrome autofill ([#6140](https://github.com/n8n-io/n8n/issues/6140)) ([c63181b](https://github.com/n8n-io/n8n/commit/c63181b3171040c3dd3051c2a1358aea0af6bae0)) +* **editor:** Fix missing `Stop Listening` button ([#6125](https://github.com/n8n-io/n8n/issues/6125)) ([20a72bb](https://github.com/n8n-io/n8n/commit/20a72bb28b981e9c8d12dd6398d843b39d80daac)) +* **editor:** Fix quote handling on dollar-sign variable completions ([#6128](https://github.com/n8n-io/n8n/issues/6128)) ([51f5990](https://github.com/n8n-io/n8n/commit/51f59905591fa492017fc3ced46601eeca5fb057)) +* **editor:** Fix sidebar button styling ([#6138](https://github.com/n8n-io/n8n/issues/6138)) ([a72a511](https://github.com/n8n-io/n8n/commit/a72a5112f34a0d8ab248f687c74b758c8db6729c)) +* **editor:** Fix unique names for node duplication ([#6134](https://github.com/n8n-io/n8n/issues/6134)) ([71ae6c6](https://github.com/n8n-io/n8n/commit/71ae6c66ef32ba86edf0bb9cdb9f24a6d40ee80c)) +* **editor:** Fix unscrollable node settings ([#6133](https://github.com/n8n-io/n8n/issues/6133)) ([c8ff368](https://github.com/n8n-io/n8n/commit/c8ff368fc7be58e7c42746f7e7a4c5f6a4149d3e)) +* **editor:** Loading state for executions tab ([#6100](https://github.com/n8n-io/n8n/issues/6100)) ([4cbb05b](https://github.com/n8n-io/n8n/commit/4cbb05b0017ffd77eca51fc5b9c5c4868515a60d)) +* **editor:** Remove pagination from binary data output ([#6093](https://github.com/n8n-io/n8n/issues/6093)) ([c6e665a](https://github.com/n8n-io/n8n/commit/c6e665a975958c433d7991c057a3e4be644daff1)) +* **editor:** Restrict `[empty]` in parameter input hint to zero-length string ([#6003](https://github.com/n8n-io/n8n/issues/6003)) ([8862e1e](https://github.com/n8n-io/n8n/commit/8862e1e7df0be62ab3746b70e613ffd2ab26bc4a)) +* **editor:** Show error in RLC if credentials are not set ([#6108](https://github.com/n8n-io/n8n/issues/6108)) ([2c240a0](https://github.com/n8n-io/n8n/commit/2c240a0e4ecd9157dca612d98a8a7c68a65a9909)) +* **HTTP Request Node:** Add description for 'Specify Body' option ([#6114](https://github.com/n8n-io/n8n/issues/6114)) ([af097ae](https://github.com/n8n-io/n8n/commit/af097ae22c7e87918ada2527c6a2fe62cb8f318a)) +* **HTTP Request Node:** Always lowercase headers ([983e6e1](https://github.com/n8n-io/n8n/commit/983e6e124eb9557eec55c5f2e2b834a926243955)) +* **Mattermost Node:** Fix base url trailing slash error ([#6097](https://github.com/n8n-io/n8n/issues/6097)) ([25a386d](https://github.com/n8n-io/n8n/commit/25a386dd70df516090e622d921a79456fc7d16e3)) +* **Merge Node:** Do not error if expected key is missing ([d219af7](https://github.com/n8n-io/n8n/commit/d219af75cf37c603c34b1ca5851cafd4a490889c)) +* Prevent displaying an endless timer in the execution list for finished executions ([#6137](https://github.com/n8n-io/n8n/issues/6137)) ([701105e](https://github.com/n8n-io/n8n/commit/701105edcf5284f276fe146d8267e1a5560ab186)) +* Prevent invocations of 'GET /rest/license' from returning an error when ephemeral licenses are used ([#6154](https://github.com/n8n-io/n8n/issues/6154)) ([a3d26ef](https://github.com/n8n-io/n8n/commit/a3d26eff79013642865fa59078732526850b96a6)) +* **Slack Node:** Restore ability to send text in addition of blocks or attachments ([8669f95](https://github.com/n8n-io/n8n/commit/8669f95736797da4f3efd33468cdeac5d28667b0)) + + +### Features + +* **core:** Add notice to alert users a new version is available ([cb497fb](https://github.com/n8n-io/n8n/commit/cb497fbbecdba670d5121fa2c6eaf7c66d8a8a38)) +* **editor:** Add support for `loadOptionsDependsOn` to RLC ([#6101](https://github.com/n8n-io/n8n/issues/6101)) ([b17d5f9](https://github.com/n8n-io/n8n/commit/b17d5f9aa086bf408e8450244460ada57de0d7c3)) +* **editor:** Add version controls settings (WIP) ([#6036](https://github.com/n8n-io/n8n/issues/6036)) ([0c9ce3a](https://github.com/n8n-io/n8n/commit/0c9ce3a2ec9487b4eb9130651927e91dcd0f85af)) +* **Item Lists Node:** Split out items work on objects as well as arrays ([c65ac03](https://github.com/n8n-io/n8n/commit/c65ac0336821868c289adc55abab40017b1856da)) +* **Microsoft Excel 365 Node:** Overhaul ([5364a2d](https://github.com/n8n-io/n8n/commit/5364a2dff32e05147b8e9dd392038eb36791e5dc)) + + + +## [0.226.2](https://github.com/n8n-io/n8n/compare/n8n@0.226.1...n8n@0.226.2) (2023-05-03) + + +### Bug Fixes + +* **core:** Fix bug running addUserActivatedColumn migration on MariaDB ([#6157](https://github.com/n8n-io/n8n/issues/6157)) ([aa8e96d](https://github.com/n8n-io/n8n/commit/aa8e96dd6b19f105a957da71a5c4d7ab5caecc01)) + + + +## [0.226.1](https://github.com/n8n-io/n8n/compare/n8n@0.226.0...n8n@0.226.1) (2023-05-02) + + +### Bug Fixes + +* **Compression Node:** Fix issue with decompression failing with uppercase extensions ([#6098](https://github.com/n8n-io/n8n/issues/6098)) ([7136500](https://github.com/n8n-io/n8n/commit/71365002daa71c5fa5e68a5bb373ee200a05b7b9)) +* **core:** Account for nodes with renamable content ([#6109](https://github.com/n8n-io/n8n/issues/6109)) ([b561d46](https://github.com/n8n-io/n8n/commit/b561d463265831f3cd370ec99982847f2bddca41)) +* **core:** Fix `hasOwnProperty` on augmented objects ([#6124](https://github.com/n8n-io/n8n/issues/6124)) ([2f015c0](https://github.com/n8n-io/n8n/commit/2f015c0f153785384b009e71bf4994e18b5d06b8)) +* **core:** Fix canceled execution status ([#6142](https://github.com/n8n-io/n8n/issues/6142)) ([1796101](https://github.com/n8n-io/n8n/commit/1796101fed03d04906f2341a0488b60b7b5bf71c)) +* **core:** Skip auth for controllers/routes that don't use the `Authorized` decorator, or use `Authorized('none')` ([#6106](https://github.com/n8n-io/n8n/issues/6106)) ([9d44991](https://github.com/n8n-io/n8n/commit/9d44991b2a8c9384e2bf32204ac47a4ecb0131be)) +* Correctly allow sharees to test credential when opening the modal ([#6111](https://github.com/n8n-io/n8n/issues/6111)) ([240bb47](https://github.com/n8n-io/n8n/commit/240bb47e8e2cd99c56b8837079073bbf2bf5f687)) +* **Date & Time Node:** Numbers conversions fix ([e11e7cd](https://github.com/n8n-io/n8n/commit/e11e7cd603a145c83c36f6a6deb821a56ccabd6f)) +* **editor:** Change execution list tab loader design ([#6120](https://github.com/n8n-io/n8n/issues/6120)) ([ffc033f](https://github.com/n8n-io/n8n/commit/ffc033ff8ff391be09da8dbd62f8eb06a94f6cb0)) +* **editor:** Fix `Show details` summary ([#6113](https://github.com/n8n-io/n8n/issues/6113)) ([e12bafb](https://github.com/n8n-io/n8n/commit/e12bafb9473393dd9e139d0e0a4a21241b417645)) +* **editor:** Fix copy selection behavior ([#6112](https://github.com/n8n-io/n8n/issues/6112)) ([0efd94a](https://github.com/n8n-io/n8n/commit/0efd94a875fe9e3cac5da3421effcf6c8f6eaae8)) +* **editor:** Fix cropped off completions docstrings ([#6129](https://github.com/n8n-io/n8n/issues/6129)) ([06594cc](https://github.com/n8n-io/n8n/commit/06594cc36f1a0d4069e556d6dbb4e268a78301c6)) +* **editor:** Fix missing `Stop Listening` button ([#6125](https://github.com/n8n-io/n8n/issues/6125)) ([dcbd2d2](https://github.com/n8n-io/n8n/commit/dcbd2d2bc1c59ca8bbe6a802a2a8f482b3d20d74)) +* **editor:** Fix quote handling on dollar-sign variable completions ([#6128](https://github.com/n8n-io/n8n/issues/6128)) ([c23ad35](https://github.com/n8n-io/n8n/commit/c23ad3502d0a8c4bcbd60cce2fc13a91981fb119)) +* **editor:** Fix sidebar button styling ([#6138](https://github.com/n8n-io/n8n/issues/6138)) ([d3f4bc1](https://github.com/n8n-io/n8n/commit/d3f4bc1859104f11d2cf38ef3f3405d62c88bc6d)) +* **editor:** Fix unique names for node duplication ([#6134](https://github.com/n8n-io/n8n/issues/6134)) ([48a4068](https://github.com/n8n-io/n8n/commit/48a4068d7ec1ddf5d5a4d10620dce1c89cb8fa34)) +* **editor:** Fix unscrollable node settings ([#6133](https://github.com/n8n-io/n8n/issues/6133)) ([f762f16](https://github.com/n8n-io/n8n/commit/f762f16afb7b9ea54e29950c06133a292dc87b3f)) +* **editor:** Loading state for executions tab ([#6100](https://github.com/n8n-io/n8n/issues/6100)) ([2e12c50](https://github.com/n8n-io/n8n/commit/2e12c50477014c57fa665ba36f48ff658cf7ee94)) +* **editor:** Remove pagination from binary data output ([#6093](https://github.com/n8n-io/n8n/issues/6093)) ([7b7d9de](https://github.com/n8n-io/n8n/commit/7b7d9de7586905b2740541e7f0289bc78f8f2ad7)) +* **editor:** Show error in RLC if credentials are not set ([#6108](https://github.com/n8n-io/n8n/issues/6108)) ([5bf3400](https://github.com/n8n-io/n8n/commit/5bf3400ca7ffa04bb51214bc539435b847614cbb)) +* **HTTP Request Node:** Add description for 'Specify Body' option ([#6114](https://github.com/n8n-io/n8n/issues/6114)) ([69b6ba8](https://github.com/n8n-io/n8n/commit/69b6ba85202c5a28052a56c041b880bfa9fcf0a0)) +* **HTTP Request Node:** Always lowercase headers ([31c56a1](https://github.com/n8n-io/n8n/commit/31c56a12f273d5569ed3ac4d72cf74c90cc24b31)) +* **Mattermost Node:** Fix base url trailing slash error ([#6097](https://github.com/n8n-io/n8n/issues/6097)) ([788fda1](https://github.com/n8n-io/n8n/commit/788fda1b7dbf29d7a7e614a959ac3630b2a6559f)) +* **Merge Node:** Do not error if expected key is missing ([8b59564](https://github.com/n8n-io/n8n/commit/8b59564776ebaf2c54b9e2dae5b77109895da883)) +* Prevent displaying an endless timer in the execution list for finished executions ([#6137](https://github.com/n8n-io/n8n/issues/6137)) ([2672896](https://github.com/n8n-io/n8n/commit/2672896c8e2de7d1f92899a392a3f2b3f60aaef3)) +* **Slack Node:** Restore ability to send text in addition of blocks or attachments ([625d672](https://github.com/n8n-io/n8n/commit/625d6729b4158fbc811941ce45819f32372e6265)) + + + # [0.226.0](https://github.com/n8n-io/n8n/compare/n8n@0.225.0...n8n@0.226.0) (2023-04-26) diff --git a/cypress.config.js b/cypress.config.js index d1451fcd06fc2..b6cea71083e09 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -11,7 +11,7 @@ module.exports = defineConfig({ }, defaultCommandTimeout: 10000, requestTimeout: 12000, - numTestsKeptInMemory: 0, + numTestsKeptInMemory: 2, experimentalMemoryManagement: true, e2e: { baseUrl: BASE_URL, @@ -35,8 +35,13 @@ module.exports = defineConfig({ return null } }, - 'enable-feature': (feature) => - fetch(BASE_URL + `/e2e/enable-feature/${feature}`, { method: 'POST' }), + 'set-feature': ({ feature, enabled }) => { + return fetch(BASE_URL + `/e2e/feature/${feature}`, { + method: 'PATCH', + body: JSON.stringify({ enabled }), + headers: { 'Content-Type': 'application/json' } + }) + }, }); }, }, diff --git a/cypress/e2e/14-mapping.cy.ts b/cypress/e2e/14-mapping.cy.ts index 8c4d6f9cdbe05..afab75190e641 100644 --- a/cypress/e2e/14-mapping.cy.ts +++ b/cypress/e2e/14-mapping.cy.ts @@ -205,7 +205,7 @@ describe('Data mapping', () => { 'have.text', `{{ $node['${SCHEDULE_TRIGGER_NODE_NAME}'].json.input[0].count }} {{ $node['${SCHEDULE_TRIGGER_NODE_NAME}'].json.input }}`, ); - ndv.getters.parameterExpressionPreview('value').should('include.text', '[empty]'); + ndv.getters.parameterExpressionPreview('value').should('have.text', ' '); ndv.actions.selectInputNode('Set'); diff --git a/cypress/e2e/23-variables.cy.ts b/cypress/e2e/23-variables.cy.ts index 8dc16bb8e967d..ce78f8fbe3db5 100644 --- a/cypress/e2e/23-variables.cy.ts +++ b/cypress/e2e/23-variables.cy.ts @@ -16,6 +16,7 @@ describe('Variables', () => { }); it('should show the unlicensed action box when the feature is disabled', () => { + cy.disableFeature('feat:variables'); cy.signin({ email, password }); cy.visit(variablesPage.url); @@ -30,7 +31,10 @@ describe('Variables', () => { beforeEach(() => { cy.signin({ email, password }); + cy.intercept('GET', '/rest/variables').as('loadVariables'); + cy.visit(variablesPage.url); + cy.wait(['@loadVariables', '@loadSettings']); }); it('should show the licensed action box when the feature is enabled', () => { diff --git a/cypress/e2e/3-default-owner.cy.ts b/cypress/e2e/3-default-owner.cy.ts index 1871dd9c6c950..6aba65180c5b4 100644 --- a/cypress/e2e/3-default-owner.cy.ts +++ b/cypress/e2e/3-default-owner.cy.ts @@ -84,11 +84,12 @@ describe('Default owner', () => { }); it('should be able to setup instance and migrate workflows and credentials', () => { - cy.setup({ email, firstName, lastName, password }); + cy.setup({ email, firstName, lastName, password }, true); messageBox.getters.content().should('contain.text', '1 existing workflow and 1 credential'); messageBox.actions.confirm(); + cy.wait('@setupRequest'); cy.url().should('include', settingsUsersPage.url); settingsSidebar.actions.back(); diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 70d8a1988234c..d0e9ddbaa0347 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -105,18 +105,22 @@ Cypress.Commands.add('signup', ({ firstName, lastName, password, url }) => { signupPage.getters.form().within(() => { cy.url().then((url) => { + cy.intercept('/rest/users/*').as('userSignup') signupPage.getters.firstName().type(firstName); signupPage.getters.lastName().type(lastName); signupPage.getters.password().type(password); signupPage.getters.submit().click(); + cy.wait('@userSignup'); }); }); }); -Cypress.Commands.add('setup', ({ email, firstName, lastName, password }) => { +Cypress.Commands.add('setup', ({ email, firstName, lastName, password }, skipIntercept = false) => { const signupPage = new SignupPage(); + cy.intercept('GET', signupPage.url).as('setupPage'); cy.visit(signupPage.url); + cy.wait('@setupPage'); signupPage.getters.form().within(() => { cy.url().then((url) => { @@ -125,7 +129,13 @@ Cypress.Commands.add('setup', ({ email, firstName, lastName, password }) => { signupPage.getters.firstName().type(firstName); signupPage.getters.lastName().type(lastName); signupPage.getters.password().type(password); + + cy.intercept('POST', '/rest/owner/setup').as('setupRequest'); signupPage.getters.submit().click(); + + if(!skipIntercept) { + cy.wait('@setupRequest'); + } } else { cy.log('User already signed up'); } @@ -168,7 +178,9 @@ Cypress.Commands.add('skipSetup', () => { const workflowPage = new WorkflowPage(); const Confirmation = new MessageBox(); + cy.intercept('GET', signupPage.url).as('setupPage'); cy.visit(signupPage.url); + cy.wait('@setupPage'); signupPage.getters.form().within(() => { cy.url().then((url) => { @@ -199,7 +211,11 @@ Cypress.Commands.add('setupOwner', (payload) => { }); Cypress.Commands.add('enableFeature', (feature) => { - cy.task('enable-feature', feature); + cy.task('set-feature', { feature, enabled: true }); +}); + +Cypress.Commands.add('disableFeature', (feature) => { + cy.task('set-feature', { feature, enabled: false }); }); Cypress.Commands.add('grantBrowserPermissions', (...permissions: string[]) => { diff --git a/cypress/support/index.ts b/cypress/support/index.ts index 7665602dafcb4..7b1b15db2b962 100644 --- a/cypress/support/index.ts +++ b/cypress/support/index.ts @@ -36,13 +36,14 @@ declare global { signin(payload: SigninPayload): void; signout(): void; signup(payload: SignupPayload): void; - setup(payload: SetupPayload): void; + setup(payload: SetupPayload, skipIntercept?: boolean): void; setupOwner(payload: SetupPayload): void; inviteUsers(payload: InviteUsersPayload): void; interceptREST(method: string, url: string): Chainable; skipSetup(): void; resetAll(): void; enableFeature(feature: string): void; + disableFeature(feature: string): void; waitForLoad(waitForIntercepts?: boolean): void; grantBrowserPermissions(...permissions: string[]): void; readClipboard(): Chainable; diff --git a/package.json b/package.json index a478b32076234..7bbcaa3420f48 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "n8n", - "version": "0.226.0", + "version": "0.227.0", "private": true, "homepage": "https://n8n.io", "engines": { diff --git a/packages/@n8n_io/eslint-config/base.js b/packages/@n8n_io/eslint-config/base.js index 6c89ea6e52298..0958755fc19be 100644 --- a/packages/@n8n_io/eslint-config/base.js +++ b/packages/@n8n_io/eslint-config/base.js @@ -6,9 +6,7 @@ const config = (module.exports = { 'node_modules/**', 'dist/**', // TODO: remove these - 'test/**', - '.eslintrc.js', - 'jest.config.js', + '*.js', ], plugins: [ @@ -452,6 +450,43 @@ const config = (module.exports = { '@typescript-eslint/no-unused-vars': 'off', }, }, + { + files: ['test/**/*.ts'], + // TODO: Remove these + rules: { + '@typescript-eslint/await-thenable': 'off', + '@typescript-eslint/ban-ts-comment': 'off', + '@typescript-eslint/naming-convention': 'off', + '@typescript-eslint/no-duplicate-imports': 'off', + '@typescript-eslint/no-empty-function': 'off', + '@typescript-eslint/no-floating-promises': 'off', + '@typescript-eslint/no-loop-func': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/no-shadow': 'off', + '@typescript-eslint/no-throw-literal': 'off', + '@typescript-eslint/no-unsafe-argument': 'off', + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-unsafe-call': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/no-unsafe-return': 'off', + '@typescript-eslint/no-unused-expressions': 'off', + '@typescript-eslint/no-unused-vars': 'off', + '@typescript-eslint/no-use-before-define': 'off', + '@typescript-eslint/no-var-requires': 'off', + '@typescript-eslint/prefer-nullish-coalescing': 'off', + '@typescript-eslint/prefer-optional-chain': 'off', + '@typescript-eslint/restrict-plus-operands': 'off', + '@typescript-eslint/restrict-template-expressions': 'off', + '@typescript-eslint/unbound-method': 'off', + 'id-denylist': 'off', + 'import/no-cycle': 'off', + 'import/no-default-export': 'off', + 'import/no-extraneous-dependencies': 'off', + 'n8n-local-rules/no-uncaught-json-parse': 'off', + 'prefer-const': 'off', + 'prefer-spread': 'off', + }, + }, ], }); diff --git a/packages/cli/package.json b/packages/cli/package.json index fac909bf86cee..39112605e2851 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "n8n", - "version": "0.226.0", + "version": "0.227.0", "description": "n8n Workflow Automation Tool", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", diff --git a/packages/cli/src/ActiveWorkflowRunner.ts b/packages/cli/src/ActiveWorkflowRunner.ts index 2cd770291e7bd..ca175b303b715 100644 --- a/packages/cli/src/ActiveWorkflowRunner.ts +++ b/packages/cli/src/ActiveWorkflowRunner.ts @@ -66,6 +66,7 @@ import { ExternalHooks } from '@/ExternalHooks'; import { whereClause } from './UserManagement/UserManagementHelper'; import { WorkflowsService } from './workflows/workflows.services'; import { START_NODES } from './constants'; +import { webhookNotFoundErrorMessage } from './utils'; const WEBHOOK_PROD_UNREGISTERED_HINT = "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"; @@ -221,7 +222,7 @@ export class ActiveWorkflowRunner { if (dynamicWebhooks === undefined || dynamicWebhooks.length === 0) { // The requested webhook is not registered throw new ResponseHelper.NotFoundError( - `The requested webhook "${httpMethod} ${path}" is not registered.`, + webhookNotFoundErrorMessage(path, httpMethod), WEBHOOK_PROD_UNREGISTERED_HINT, ); } @@ -247,7 +248,7 @@ export class ActiveWorkflowRunner { }); if (webhook === null) { throw new ResponseHelper.NotFoundError( - `The requested webhook "${httpMethod} ${path}" is not registered.`, + webhookNotFoundErrorMessage(path, httpMethod), WEBHOOK_PROD_UNREGISTERED_HINT, ); } diff --git a/packages/cli/src/InternalHooks.ts b/packages/cli/src/InternalHooks.ts index dd29b0874e828..55793c0528d77 100644 --- a/packages/cli/src/InternalHooks.ts +++ b/packages/cli/src/InternalHooks.ts @@ -290,6 +290,10 @@ export class InternalHooks implements IInternalHooksClass { properties.user_id = userId; } + if (runData?.data.resultData.error?.message?.includes('canceled')) { + runData.status = 'canceled'; + } + properties.success = !!runData?.finished; let executionStatus: ExecutionStatus; @@ -297,6 +301,8 @@ export class InternalHooks implements IInternalHooksClass { executionStatus = 'crashed'; } else if (runData?.status === 'waiting' || runData?.data?.waitTill) { executionStatus = 'waiting'; + } else if (runData?.status === 'canceled') { + executionStatus = 'canceled'; } else { executionStatus = properties.success ? 'success' : 'failed'; } diff --git a/packages/cli/src/License.ts b/packages/cli/src/License.ts index 1b820931e1ed6..c00e1b1835691 100644 --- a/packages/cli/src/License.ts +++ b/packages/cli/src/License.ts @@ -168,8 +168,7 @@ export class License { } return entitlements.find( - (entitlement) => - (entitlement.productMetadata.terms as unknown as { isMainPlan: boolean }).isMainPlan, + (entitlement) => (entitlement.productMetadata?.terms as { isMainPlan?: boolean })?.isMainPlan, ); } diff --git a/packages/cli/src/TestWebhooks.ts b/packages/cli/src/TestWebhooks.ts index e3b7b4fb035c2..d509a1c030e3f 100644 --- a/packages/cli/src/TestWebhooks.ts +++ b/packages/cli/src/TestWebhooks.ts @@ -18,6 +18,7 @@ import type { IResponseCallbackData, IWorkflowDb } from '@/Interfaces'; import { Push } from '@/push'; import * as ResponseHelper from '@/ResponseHelper'; import * as WebhookHelpers from '@/WebhookHelpers'; +import { webhookNotFoundErrorMessage } from './utils'; const WEBHOOK_TEST_UNREGISTERED_HINT = "Click the 'Execute workflow' button on the canvas, then try again. (In test mode, the webhook only works for one call after you click this button)"; @@ -69,8 +70,9 @@ export class TestWebhooks { webhookData = activeWebhooks.get(httpMethod, pathElements.join('/'), webhookId); if (webhookData === undefined) { // The requested webhook is not registered + const methods = await this.getWebhookMethods(path); throw new ResponseHelper.NotFoundError( - `The requested webhook "${httpMethod} ${path}" is not registered.`, + webhookNotFoundErrorMessage(path, httpMethod, methods), WEBHOOK_TEST_UNREGISTERED_HINT, ); } @@ -95,8 +97,9 @@ export class TestWebhooks { // TODO: Clean that duplication up one day and improve code generally if (testWebhookData[webhookKey] === undefined) { // The requested webhook is not registered + const methods = await this.getWebhookMethods(path); throw new ResponseHelper.NotFoundError( - `The requested webhook "${httpMethod} ${path}" is not registered.`, + webhookNotFoundErrorMessage(path, httpMethod, methods), WEBHOOK_TEST_UNREGISTERED_HINT, ); } @@ -160,7 +163,7 @@ export class TestWebhooks { if (!webhookMethods.length) { // The requested webhook is not registered throw new ResponseHelper.NotFoundError( - `The requested webhook "${path}" is not registered.`, + webhookNotFoundErrorMessage(path), WEBHOOK_TEST_UNREGISTERED_HINT, ); } diff --git a/packages/cli/src/WorkflowExecuteAdditionalData.ts b/packages/cli/src/WorkflowExecuteAdditionalData.ts index f958d3a49508d..7f6f26f64b51b 100644 --- a/packages/cli/src/WorkflowExecuteAdditionalData.ts +++ b/packages/cli/src/WorkflowExecuteAdditionalData.ts @@ -583,9 +583,12 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks { } const workflowHasCrashed = fullRunData.status === 'crashed'; - const workflowDidSucceed = !fullRunData.data.resultData.error && !workflowHasCrashed; + const workflowWasCanceled = fullRunData.status === 'canceled'; + const workflowDidSucceed = + !fullRunData.data.resultData.error && !workflowHasCrashed && !workflowWasCanceled; let workflowStatusFinal: ExecutionStatus = workflowDidSucceed ? 'success' : 'failed'; if (workflowHasCrashed) workflowStatusFinal = 'crashed'; + if (workflowWasCanceled) workflowStatusFinal = 'canceled'; if ( (workflowDidSucceed && saveDataSuccessExecution === 'none') || @@ -755,9 +758,12 @@ function hookFunctionsSaveWorker(): IWorkflowExecuteHooks { } const workflowHasCrashed = fullRunData.status === 'crashed'; - const workflowDidSucceed = !fullRunData.data.resultData.error && !workflowHasCrashed; + const workflowWasCanceled = fullRunData.status === 'canceled'; + const workflowDidSucceed = + !fullRunData.data.resultData.error && !workflowHasCrashed && !workflowWasCanceled; let workflowStatusFinal: ExecutionStatus = workflowDidSucceed ? 'success' : 'failed'; if (workflowHasCrashed) workflowStatusFinal = 'crashed'; + if (workflowWasCanceled) workflowStatusFinal = 'canceled'; if (!workflowDidSucceed) { executeErrorWorkflow( diff --git a/packages/cli/src/WorkflowRunnerProcess.ts b/packages/cli/src/WorkflowRunnerProcess.ts index 3b61ed11fab82..0e4bb08137e13 100644 --- a/packages/cli/src/WorkflowRunnerProcess.ts +++ b/packages/cli/src/WorkflowRunnerProcess.ts @@ -472,6 +472,8 @@ process.on('message', async (message: IProcessMessage) => { ? new WorkflowOperationError('Workflow execution timed out!') : new WorkflowOperationError('Workflow-Execution has been canceled!'); + runData.status = message.type === 'timeout' ? 'failed' : 'canceled'; + // If there is any data send it to parent process, if execution timedout add the error await workflowRunner.workflowExecute.processSuccessExecution( workflowRunner.startedAt, diff --git a/packages/cli/src/api/e2e.api.ts b/packages/cli/src/api/e2e.api.ts index 73adae880dbf9..2f9bb6ee1fefc 100644 --- a/packages/cli/src/api/e2e.api.ts +++ b/packages/cli/src/api/e2e.api.ts @@ -36,33 +36,30 @@ type Feature = keyof typeof enabledFeatures; Container.get(License).isFeatureEnabled = (feature: Feature) => enabledFeatures[feature] ?? false; -const tablesToTruncate = [ - 'auth_identity', - 'auth_provider_sync_history', - 'event_destinations', - 'shared_workflow', - 'shared_credentials', - 'webhook_entity', - 'workflows_tags', - 'credentials_entity', - 'tag_entity', - 'workflow_statistics', - 'workflow_entity', - 'execution_entity', - 'settings', - 'installed_packages', - 'installed_nodes', - 'user', - 'role', -]; +const tablesNotToTruncate = ['sqlite_sequence']; const truncateAll = async () => { const connection = Db.getConnection(); - for (const table of tablesToTruncate) { - await connection.query( - `DELETE FROM ${table}; DELETE FROM sqlite_sequence WHERE name=${table};`, - ); + const allTables: Array<{ name: string }> = await connection.query( + "SELECT name FROM sqlite_master WHERE type='table';", + ); + + // Disable foreign key constraint checks + await connection.query('PRAGMA foreign_keys = OFF;'); + + for (const { name: table } of allTables) { + try { + if (tablesNotToTruncate.includes(table)) continue; + await connection.query( + `DELETE FROM ${table}; DELETE FROM sqlite_sequence WHERE name=${table};`, + ); + } catch (error) { + console.warn('Dropping Table for E2E Reset error: ', error); + } } + + // Re-enable foreign key constraint checks + await connection.query('PRAGMA foreign_keys = ON;'); }; const setupUserManagement = async () => { @@ -139,8 +136,14 @@ e2eController.post('/db/setup-owner', bodyParser.json(), async (req, res) => { res.writeHead(204).end(); }); -e2eController.post('/enable-feature/:feature', async (req: Request<{ feature: Feature }>, res) => { - const { feature } = req.params; - enabledFeatures[feature] = true; - res.writeHead(204).end(); -}); +e2eController.patch( + '/feature/:feature', + bodyParser.json(), + async (req: Request<{ feature: Feature }>, res) => { + const { feature } = req.params; + const { enabled } = req.body; + + enabledFeatures[feature] = enabled === undefined || enabled === true; + res.writeHead(204).end(); + }, +); diff --git a/packages/cli/src/databases/migrations/mysqldb/1681134145996-AddUserActivatedProperty.ts b/packages/cli/src/databases/migrations/mysqldb/1681134145996-AddUserActivatedProperty.ts index e1c193a959935..b08c384c29e14 100644 --- a/packages/cli/src/databases/migrations/mysqldb/1681134145996-AddUserActivatedProperty.ts +++ b/packages/cli/src/databases/migrations/mysqldb/1681134145996-AddUserActivatedProperty.ts @@ -25,13 +25,16 @@ export class AddUserActivatedProperty1681134145996 implements MigrationInterface AND r.scope = 'workflow'`, ); - const updatedUsers = activatedUsers.map((user) => + const updatedUsers = activatedUsers.map((user) => { + /* + MariaDB returns settings as a string and MySQL as a JSON + */ + const userSettings = + typeof user.settings === 'string' ? user.settings : JSON.stringify(user.settings); queryRunner.query( - `UPDATE ${tablePrefix}user SET settings = '${JSON.stringify(user.settings)}' WHERE id = '${ - user.id - }' `, - ), - ); + `UPDATE ${tablePrefix}user SET settings = '${userSettings}' WHERE id = '${user.id}' `, + ); + }); await Promise.all(updatedUsers); diff --git a/packages/cli/src/license/license.controller.ts b/packages/cli/src/license/license.controller.ts index e5dd13a18ba9c..7fc24df18846c 100644 --- a/packages/cli/src/license/license.controller.ts +++ b/packages/cli/src/license/license.controller.ts @@ -75,25 +75,29 @@ licenseController.post( } catch (e) { const error = e as Error & { errorId?: string }; + let message = 'Failed to activate license'; + //override specific error messages (to map License Server vocabulary to n8n terms) switch (error.errorId ?? 'UNSPECIFIED') { case 'SCHEMA_VALIDATION': - error.message = 'Activation key is in the wrong format'; + message = 'Activation key is in the wrong format'; break; case 'RESERVATION_EXHAUSTED': - error.message = + message = 'Activation key has been used too many times. Please contact sales@n8n.io if you would like to extend it'; break; case 'RESERVATION_EXPIRED': - error.message = 'Activation key has expired'; + message = 'Activation key has expired'; break; case 'NOT_FOUND': case 'RESERVATION_CONFLICT': - error.message = 'Activation key not found'; + message = 'Activation key not found'; break; + default: + getLogger().error(message, { stack: error.stack ?? 'n/a' }); } - throw new ResponseHelper.BadRequestError(error.message); + throw new ResponseHelper.BadRequestError(message); } // Return the read data, plus the management JWT diff --git a/packages/cli/src/sso/saml/saml.service.ee.ts b/packages/cli/src/sso/saml/saml.service.ee.ts index b16d14600e1d8..cbeb3dee3ac02 100644 --- a/packages/cli/src/sso/saml/saml.service.ee.ts +++ b/packages/cli/src/sso/saml/saml.service.ee.ts @@ -211,6 +211,8 @@ export class SamlService { this._samlPreferences.metadata = fetchedMetadata; } } else if (prefs.metadata) { + // remove metadataUrl if metadata is set directly + this._samlPreferences.metadataUrl = undefined; const validationResult = await validateMetadata(prefs.metadata); if (!validationResult) { throw new Error('Invalid SAML metadata'); diff --git a/packages/cli/src/utils.ts b/packages/cli/src/utils.ts index c99ef97f500fa..c9eee2f4c857f 100644 --- a/packages/cli/src/utils.ts +++ b/packages/cli/src/utils.ts @@ -58,3 +58,31 @@ export const separate = (array: T[], test: (element: T) => boolean) => { return [pass, fail]; }; + +export const webhookNotFoundErrorMessage = ( + path: string, + httpMethod?: string, + webhookMethods?: string[], +) => { + let webhookPath = path; + + if (httpMethod) { + webhookPath = `${httpMethod} ${webhookPath}`; + } + + if (webhookMethods?.length && httpMethod) { + let methods = ''; + + if (webhookMethods.length === 1) { + methods = webhookMethods[0]; + } else { + const lastMethod = webhookMethods.pop(); + + methods = `${webhookMethods.join(', ')} or ${lastMethod as string}`; + } + + return `This webhook is not registered for ${httpMethod} requests. Did you mean to make a ${methods} request?`; + } else { + return `The requested webhook "${webhookPath}" is not registered.`; + } +}; diff --git a/packages/cli/test/integration/commands/import.cmd.test.ts b/packages/cli/test/integration/commands/import.cmd.test.ts index bc00a5e53d481..93fbfeac8f35c 100644 --- a/packages/cli/test/integration/commands/import.cmd.test.ts +++ b/packages/cli/test/integration/commands/import.cmd.test.ts @@ -25,7 +25,7 @@ test('import:workflow should import active workflow and deactivate it', async () ['--separate', '--input=./test/integration/commands/importWorkflows/separate'], config, ); - const mockExit = jest.spyOn(process, 'exit').mockImplementation((number) => { + const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => { throw new Error('process.exit'); }); @@ -52,7 +52,7 @@ test('import:workflow should import active workflow from combined file and deact ['--input=./test/integration/commands/importWorkflows/combined/combined.json'], config, ); - const mockExit = jest.spyOn(process, 'exit').mockImplementation((number) => { + const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => { throw new Error('process.exit'); }); diff --git a/packages/cli/test/integration/credentials.ee.test.ts b/packages/cli/test/integration/credentials.ee.test.ts index 139975eb5bb42..61409984a5e40 100644 --- a/packages/cli/test/integration/credentials.ee.test.ts +++ b/packages/cli/test/integration/credentials.ee.test.ts @@ -415,7 +415,7 @@ describe('PUT /credentials/:id/share', () => { test('should respond 403 for non-existing credentials', async () => { const response = await authOwnerAgent - .put(`/credentials/1234567/share`) + .put('/credentials/1234567/share') .send({ shareWithIds: [member.id] }); expect(response.statusCode).toBe(403); diff --git a/packages/cli/test/integration/credentials.test.ts b/packages/cli/test/integration/credentials.test.ts index 3a2f5f8415218..3f5b7e74a7866 100644 --- a/packages/cli/test/integration/credentials.test.ts +++ b/packages/cli/test/integration/credentials.test.ts @@ -11,9 +11,8 @@ import type { Role } from '@db/entities/Role'; import type { User } from '@db/entities/User'; import { randomCredentialPayload, randomName, randomString } from './shared/random'; import * as testDb from './shared/testDb'; -import type { SaveCredentialFunction } from './shared/types'; +import type { AuthAgent, SaveCredentialFunction } from './shared/types'; import * as utils from './shared/utils'; -import type { AuthAgent } from './shared/types'; // mock that credentialsSharing is not enabled const mockIsCredentialsSharingEnabled = jest.spyOn(UserManagementHelpers, 'isSharingEnabled'); @@ -124,7 +123,7 @@ describe('POST /credentials', () => { expect(credential.name).toBe(payload.name); expect(credential.type).toBe(payload.type); - expect(credential.nodesAccess[0].nodeType).toBe(payload.nodesAccess![0].nodeType); + expect(credential.nodesAccess[0].nodeType).toBe(payload.nodesAccess[0].nodeType); expect(credential.data).not.toBe(payload.data); const sharedCredential = await Db.collections.SharedCredentials.findOneOrFail({ @@ -278,7 +277,7 @@ describe('PATCH /credentials/:id', () => { expect(credential.name).toBe(patchPayload.name); expect(credential.type).toBe(patchPayload.type); - expect(credential.nodesAccess[0].nodeType).toBe(patchPayload.nodesAccess![0].nodeType); + expect(credential.nodesAccess[0].nodeType).toBe(patchPayload.nodesAccess[0].nodeType); expect(credential.data).not.toBe(patchPayload.data); const sharedCredential = await Db.collections.SharedCredentials.findOneOrFail({ @@ -315,7 +314,7 @@ describe('PATCH /credentials/:id', () => { expect(credential.name).toBe(patchPayload.name); expect(credential.type).toBe(patchPayload.type); - expect(credential.nodesAccess[0].nodeType).toBe(patchPayload.nodesAccess![0].nodeType); + expect(credential.nodesAccess[0].nodeType).toBe(patchPayload.nodesAccess[0].nodeType); expect(credential.data).not.toBe(patchPayload.data); const sharedCredential = await Db.collections.SharedCredentials.findOneOrFail({ @@ -352,7 +351,7 @@ describe('PATCH /credentials/:id', () => { expect(credential.name).toBe(patchPayload.name); expect(credential.type).toBe(patchPayload.type); - expect(credential.nodesAccess[0].nodeType).toBe(patchPayload.nodesAccess![0].nodeType); + expect(credential.nodesAccess[0].nodeType).toBe(patchPayload.nodesAccess[0].nodeType); expect(credential.data).not.toBe(patchPayload.data); const sharedCredential = await Db.collections.SharedCredentials.findOneOrFail({ diff --git a/packages/cli/test/integration/eventbus.test.ts b/packages/cli/test/integration/eventbus.test.ts index a5b712321747d..8c0b9b86eb5a1 100644 --- a/packages/cli/test/integration/eventbus.test.ts +++ b/packages/cli/test/integration/eventbus.test.ts @@ -1,4 +1,4 @@ -import express from 'express'; +import type express from 'express'; import config from '@/config'; import axios from 'axios'; import syslog from 'syslog-client'; @@ -7,23 +7,25 @@ import { Container } from 'typedi'; import type { SuperAgentTest } from 'supertest'; import * as utils from './shared/utils'; import * as testDb from './shared/testDb'; -import { Role } from '@db/entities/Role'; -import { User } from '@db/entities/User'; +import type { Role } from '@db/entities/Role'; +import type { User } from '@db/entities/User'; +import type { + MessageEventBusDestinationSentryOptions, + MessageEventBusDestinationSyslogOptions, + MessageEventBusDestinationWebhookOptions, +} from 'n8n-workflow'; import { defaultMessageEventBusDestinationSentryOptions, defaultMessageEventBusDestinationSyslogOptions, defaultMessageEventBusDestinationWebhookOptions, - MessageEventBusDestinationSentryOptions, - MessageEventBusDestinationSyslogOptions, - MessageEventBusDestinationWebhookOptions, } from 'n8n-workflow'; import { eventBus } from '@/eventbus'; import { EventMessageGeneric } from '@/eventbus/EventMessageClasses/EventMessageGeneric'; -import { MessageEventBusDestinationSyslog } from '@/eventbus/MessageEventBusDestination/MessageEventBusDestinationSyslog.ee'; -import { MessageEventBusDestinationWebhook } from '@/eventbus/MessageEventBusDestination/MessageEventBusDestinationWebhook.ee'; -import { MessageEventBusDestinationSentry } from '@/eventbus/MessageEventBusDestination/MessageEventBusDestinationSentry.ee'; +import type { MessageEventBusDestinationSyslog } from '@/eventbus/MessageEventBusDestination/MessageEventBusDestinationSyslog.ee'; +import type { MessageEventBusDestinationWebhook } from '@/eventbus/MessageEventBusDestination/MessageEventBusDestinationWebhook.ee'; +import type { MessageEventBusDestinationSentry } from '@/eventbus/MessageEventBusDestination/MessageEventBusDestinationSentry.ee'; import { EventMessageAudit } from '@/eventbus/EventMessageClasses/EventMessageAudit'; -import { EventNamesTypes } from '@/eventbus/EventMessageClasses'; +import type { EventNamesTypes } from '@/eventbus/EventMessageClasses'; import { License } from '@/License'; jest.unmock('@/eventbus/MessageEventBus/MessageEventBus'); @@ -51,7 +53,7 @@ const testWebhookDestination: MessageEventBusDestinationWebhookOptions = { ...defaultMessageEventBusDestinationWebhookOptions, id: '88be6560-bfb4-455c-8aa1-06971e9e5522', url: 'http://localhost:3456', - method: `POST`, + method: 'POST', label: 'Test Webhook', enabled: false, subscribedEvents: ['n8n.test.message', 'n8n.audit.user.updated'], diff --git a/packages/cli/test/integration/ldap/ldap.api.test.ts b/packages/cli/test/integration/ldap/ldap.api.test.ts index eaffa15494f23..da1e5de444bcd 100644 --- a/packages/cli/test/integration/ldap/ldap.api.test.ts +++ b/packages/cli/test/integration/ldap/ldap.api.test.ts @@ -1,4 +1,4 @@ -import express from 'express'; +import type express from 'express'; import type { Entry as LdapUser } from 'ldapts'; import { Not } from 'typeorm'; import { Container } from 'typedi'; diff --git a/packages/cli/test/integration/license.api.test.ts b/packages/cli/test/integration/license.api.test.ts index 44d84e11502a7..3b45f507f4f12 100644 --- a/packages/cli/test/integration/license.api.test.ts +++ b/packages/cli/test/integration/license.api.test.ts @@ -1,7 +1,7 @@ import type { SuperAgentTest } from 'supertest'; import config from '@/config'; import type { User } from '@db/entities/User'; -import { ILicensePostResponse, ILicenseReadResponse } from '@/Interfaces'; +import type { ILicensePostResponse, ILicenseReadResponse } from '@/Interfaces'; import { License } from '@/License'; import * as testDb from './shared/testDb'; import * as utils from './shared/utils'; @@ -68,13 +68,13 @@ describe('POST /license/activate', () => { test('errors out properly', async () => { License.prototype.activate = jest.fn().mockImplementation(() => { - throw new Error(INVALID_ACIVATION_KEY_MESSAGE); + throw new Error(ACTIVATION_FAILED_MESSAGE); }); await authOwnerAgent .post('/license/activate') .send({ activationKey: 'abcde' }) - .expect(400, { code: 400, message: INVALID_ACIVATION_KEY_MESSAGE }); + .expect(400, { code: 400, message: ACTIVATION_FAILED_MESSAGE }); }); }); @@ -135,5 +135,5 @@ const DEFAULT_POST_RESPONSE: { data: ILicensePostResponse } = { }; const NON_OWNER_ACTIVATE_RENEW_MESSAGE = 'Only an instance owner may activate or renew a license'; -const INVALID_ACIVATION_KEY_MESSAGE = 'Invalid activation key'; +const ACTIVATION_FAILED_MESSAGE = 'Failed to activate license'; const RENEW_ERROR_MESSAGE = 'Something went wrong when trying to renew license'; diff --git a/packages/cli/test/integration/passwordReset.api.test.ts b/packages/cli/test/integration/passwordReset.api.test.ts index 60ccfa0ce5b46..85e3cb2d936ef 100644 --- a/packages/cli/test/integration/passwordReset.api.test.ts +++ b/packages/cli/test/integration/passwordReset.api.test.ts @@ -23,7 +23,7 @@ let globalOwnerRole: Role; let globalMemberRole: Role; let owner: User; let authlessAgent: SuperAgentTest; -let externalHooks = utils.mockInstance(ExternalHooks); +const externalHooks = utils.mockInstance(ExternalHooks); beforeAll(async () => { const app = await utils.initTestServer({ endpointGroups: ['passwordReset'] }); diff --git a/packages/cli/test/integration/publicApi/executions.test.ts b/packages/cli/test/integration/publicApi/executions.test.ts index 1d5debdbfb1e3..d71e0aa2e4da1 100644 --- a/packages/cli/test/integration/publicApi/executions.test.ts +++ b/packages/cli/test/integration/publicApi/executions.test.ts @@ -213,7 +213,7 @@ describe('GET /executions', () => { await testDb.createErrorExecution(workflow); - const response = await authOwnerAgent.get(`/executions`).query({ + const response = await authOwnerAgent.get('/executions').query({ status: 'success', }); @@ -254,7 +254,7 @@ describe('GET /executions', () => { await testDb.createErrorExecution(workflow); - const firstExecutionResponse = await authOwnerAgent.get(`/executions`).query({ + const firstExecutionResponse = await authOwnerAgent.get('/executions').query({ status: 'success', limit: 1, }); @@ -263,7 +263,7 @@ describe('GET /executions', () => { expect(firstExecutionResponse.body.data.length).toBe(1); expect(firstExecutionResponse.body.nextCursor).toBeDefined(); - const secondExecutionResponse = await authOwnerAgent.get(`/executions`).query({ + const secondExecutionResponse = await authOwnerAgent.get('/executions').query({ status: 'success', limit: 1, cursor: firstExecutionResponse.body.nextCursor, @@ -308,7 +308,7 @@ describe('GET /executions', () => { const errorExecution = await testDb.createErrorExecution(workflow); - const response = await authOwnerAgent.get(`/executions`).query({ + const response = await authOwnerAgent.get('/executions').query({ status: 'error', }); @@ -348,7 +348,7 @@ describe('GET /executions', () => { const waitingExecution = await testDb.createWaitingExecution(workflow); - const response = await authOwnerAgent.get(`/executions`).query({ + const response = await authOwnerAgent.get('/executions').query({ status: 'waiting', }); @@ -389,7 +389,7 @@ describe('GET /executions', () => { ); await testDb.createManyExecutions(2, workflow2, testDb.createSuccessfulExecution); - const response = await authOwnerAgent.get(`/executions`).query({ + const response = await authOwnerAgent.get('/executions').query({ workflowId: workflow.id, }); @@ -439,7 +439,7 @@ describe('GET /executions', () => { await testDb.createManyExecutions(2, firstWorkflowForUser2, testDb.createSuccessfulExecution); await testDb.createManyExecutions(2, secondWorkflowForUser2, testDb.createSuccessfulExecution); - const response = await authOwnerAgent.get(`/executions`); + const response = await authOwnerAgent.get('/executions'); expect(response.statusCode).toBe(200); expect(response.body.data.length).toBe(8); @@ -463,7 +463,7 @@ describe('GET /executions', () => { await testDb.createManyExecutions(2, firstWorkflowForUser2, testDb.createSuccessfulExecution); await testDb.createManyExecutions(2, secondWorkflowForUser2, testDb.createSuccessfulExecution); - const response = await authUser1Agent.get(`/executions`); + const response = await authUser1Agent.get('/executions'); expect(response.statusCode).toBe(200); expect(response.body.data.length).toBe(4); @@ -489,7 +489,7 @@ describe('GET /executions', () => { await testDb.shareWorkflowWithUsers(firstWorkflowForUser2, [user1]); - const response = await authUser1Agent.get(`/executions`); + const response = await authUser1Agent.get('/executions'); expect(response.statusCode).toBe(200); expect(response.body.data.length).toBe(6); diff --git a/packages/cli/test/integration/publicApi/workflows.test.ts b/packages/cli/test/integration/publicApi/workflows.test.ts index 02c76b616c765..c730cd07c4fc6 100644 --- a/packages/cli/test/integration/publicApi/workflows.test.ts +++ b/packages/cli/test/integration/publicApi/workflows.test.ts @@ -309,7 +309,7 @@ describe('GET /workflows/:id', () => { test('should fail due to invalid API Key', testWithAPIKey('get', '/workflows/2', 'abcXYZ')); test('should fail due to non-existing workflow', async () => { - const response = await authOwnerAgent.get(`/workflows/2`); + const response = await authOwnerAgent.get('/workflows/2'); expect(response.statusCode).toBe(404); }); @@ -375,7 +375,7 @@ describe('DELETE /workflows/:id', () => { test('should fail due to invalid API Key', testWithAPIKey('delete', '/workflows/2', 'abcXYZ')); test('should fail due to non-existing workflow', async () => { - const response = await authOwnerAgent.delete(`/workflows/2`); + const response = await authOwnerAgent.delete('/workflows/2'); expect(response.statusCode).toBe(404); }); @@ -447,7 +447,7 @@ describe('POST /workflows/:id/activate', () => { ); test('should fail due to non-existing workflow', async () => { - const response = await authOwnerAgent.post(`/workflows/2/activate`); + const response = await authOwnerAgent.post('/workflows/2/activate'); expect(response.statusCode).toBe(404); }); @@ -549,7 +549,7 @@ describe('POST /workflows/:id/deactivate', () => { ); test('should fail due to non-existing workflow', async () => { - const response = await authOwnerAgent.post(`/workflows/2/deactivate`); + const response = await authOwnerAgent.post('/workflows/2/deactivate'); expect(response.statusCode).toBe(404); }); @@ -709,7 +709,7 @@ describe('PUT /workflows/:id', () => { test('should fail due to invalid API Key', testWithAPIKey('put', '/workflows/1', 'abcXYZ')); test('should fail due to non-existing workflow', async () => { - const response = await authOwnerAgent.put(`/workflows/1`).send({ + const response = await authOwnerAgent.put('/workflows/1').send({ name: 'testing', nodes: [ { @@ -737,7 +737,7 @@ describe('PUT /workflows/:id', () => { }); test('should fail due to invalid body', async () => { - const response = await authOwnerAgent.put(`/workflows/1`).send({ + const response = await authOwnerAgent.put('/workflows/1').send({ nodes: [ { id: 'uuid-1234', diff --git a/packages/cli/test/integration/saml/saml.api.test.ts b/packages/cli/test/integration/saml/saml.api.test.ts index 08dfa72419850..a9bda3c53ffb4 100644 --- a/packages/cli/test/integration/saml/saml.api.test.ts +++ b/packages/cli/test/integration/saml/saml.api.test.ts @@ -11,8 +11,8 @@ import * as utils from '../shared/utils'; import { sampleConfig } from './sampleMetadata'; import { InternalHooks } from '@/InternalHooks'; import { SamlService } from '@/sso/saml/saml.service.ee'; -import { SamlUserAttributes } from '@/sso/saml/types/samlUserAttributes'; -import { AuthenticationMethod } from 'n8n-workflow'; +import type { SamlUserAttributes } from '@/sso/saml/types/samlUserAttributes'; +import type { AuthenticationMethod } from 'n8n-workflow'; let someUser: User; let owner: User; diff --git a/packages/cli/test/integration/shared/constants.ts b/packages/cli/test/integration/shared/constants.ts index 37798aa4d0f83..d259011696207 100644 --- a/packages/cli/test/integration/shared/constants.ts +++ b/packages/cli/test/integration/shared/constants.ts @@ -1,8 +1,8 @@ import config from '@/config'; -export const REST_PATH_SEGMENT = config.getEnv('endpoints.rest') as Readonly; +export const REST_PATH_SEGMENT = config.getEnv('endpoints.rest'); -export const PUBLIC_API_REST_PATH_SEGMENT = config.getEnv('publicApi.path') as Readonly; +export const PUBLIC_API_REST_PATH_SEGMENT = config.getEnv('publicApi.path'); export const AUTHLESS_ENDPOINTS: Readonly = [ 'healthz', diff --git a/packages/cli/test/integration/shared/testDb.ts b/packages/cli/test/integration/shared/testDb.ts index 95e7303010622..f855aff86b2a7 100644 --- a/packages/cli/test/integration/shared/testDb.ts +++ b/packages/cli/test/integration/shared/testDb.ts @@ -1,9 +1,6 @@ import { UserSettings } from 'n8n-core'; -import { - DataSource as Connection, - DataSourceOptions as ConnectionOptions, - Repository, -} from 'typeorm'; +import type { DataSourceOptions as ConnectionOptions, Repository } from 'typeorm'; +import { DataSource as Connection } from 'typeorm'; import { Container } from 'typedi'; import config from '@/config'; @@ -24,7 +21,7 @@ import type { TagEntity } from '@db/entities/TagEntity'; import type { User } from '@db/entities/User'; import type { WorkflowEntity } from '@db/entities/WorkflowEntity'; import { RoleRepository } from '@db/repositories'; -import { ICredentialsDb } from '@/Interfaces'; +import type { ICredentialsDb } from '@/Interfaces'; import { DB_INITIALIZATION_TIMEOUT } from './constants'; import { randomApiKey, randomEmail, randomName, randomString, randomValidPassword } from './random'; @@ -211,6 +208,7 @@ export async function createManyUsers( amount: number, attributes: Partial = {}, ): Promise { + // eslint-disable-next-line prefer-const let { email, password, firstName, lastName, globalRole, ...rest } = attributes; if (!globalRole) { globalRole = await getGlobalMemberRole(); diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index 9e8e4aa2b1ab8..dbd9920a517ee 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -7,25 +7,23 @@ import { CronJob } from 'cron'; import express from 'express'; import set from 'lodash.set'; import { BinaryDataManager, UserSettings } from 'n8n-core'; -import { +import type { ICredentialType, - IDataObject, IExecuteFunctions, INode, INodeExecutionData, INodeParameters, ITriggerFunctions, ITriggerResponse, - LoggerProxy, - NodeHelpers, - toCronExpression, TriggerTime, } from 'n8n-workflow'; -import superagent from 'superagent'; +import { deepCopy } from 'n8n-workflow'; +import { LoggerProxy, NodeHelpers, toCronExpression } from 'n8n-workflow'; +import type superagent from 'superagent'; import request from 'supertest'; import { URL } from 'url'; import { mock } from 'jest-mock-extended'; -import { DeepPartial } from 'ts-essentials'; +import type { DeepPartial } from 'ts-essentials'; import config from '@/config'; import * as Db from '@/Db'; import { WorkflowEntity } from '@db/entities/WorkflowEntity'; @@ -368,7 +366,7 @@ export async function initNodeTypes() { outputs: ['main'], properties: [], }, - execute(this: IExecuteFunctions): Promise { + async execute(this: IExecuteFunctions): Promise { const items = this.getInputData(); return this.prepareOutputData(items); @@ -571,7 +569,7 @@ export async function initNodeTypes() { }, ], }, - execute(this: IExecuteFunctions): Promise { + async execute(this: IExecuteFunctions): Promise { const items = this.getInputData(); if (items.length === 0) { @@ -585,13 +583,13 @@ export async function initNodeTypes() { for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { keepOnlySet = this.getNodeParameter('keepOnlySet', itemIndex, false) as boolean; item = items[itemIndex]; - const options = this.getNodeParameter('options', itemIndex, {}) as IDataObject; + const options = this.getNodeParameter('options', itemIndex, {}); const newItem: INodeExecutionData = { json: {}, }; - if (keepOnlySet !== true) { + if (!keepOnlySet) { if (item.binary !== undefined) { // Create a shallow copy of the binary data so that the old // data references which do not get changed still stay behind @@ -600,7 +598,7 @@ export async function initNodeTypes() { Object.assign(newItem.binary, item.binary); } - newItem.json = JSON.parse(JSON.stringify(item.json)); + newItem.json = deepCopy(item.json); } // Add boolean values @@ -708,7 +706,7 @@ export function createAuthAgent(app: express.Application) { * Example: http://127.0.0.1:62100/me/password → http://127.0.0.1:62100/rest/me/password */ export function prefix(pathSegment: string) { - return function (request: superagent.SuperAgentRequest) { + return async function (request: superagent.SuperAgentRequest) { const url = new URL(request.url); // enforce consistency at call sites diff --git a/packages/cli/test/integration/users.api.test.ts b/packages/cli/test/integration/users.api.test.ts index 966bb6d1694a7..3e3fe82db8405 100644 --- a/packages/cli/test/integration/users.api.test.ts +++ b/packages/cli/test/integration/users.api.test.ts @@ -514,7 +514,7 @@ describe('UserManagementMailer expect NodeMailer.verifyConnection', () => { test('not be called when SMTP not set up', async () => { const userManagementMailer = new UserManagementMailer(); // NodeMailer.verifyConnection gets called only explicitly - expect(async () => await userManagementMailer.verifyConnection()).rejects.toThrow(); + expect(async () => userManagementMailer.verifyConnection()).rejects.toThrow(); expect(NodeMailer.prototype.verifyConnection).toHaveBeenCalledTimes(0); }); @@ -526,6 +526,6 @@ describe('UserManagementMailer expect NodeMailer.verifyConnection', () => { const userManagementMailer = new UserManagementMailer(); // NodeMailer.verifyConnection gets called only explicitly - expect(async () => await userManagementMailer.verifyConnection()).not.toThrow(); + expect(async () => userManagementMailer.verifyConnection()).not.toThrow(); }); }); diff --git a/packages/cli/test/integration/variables.test.ts b/packages/cli/test/integration/variables.test.ts index 46e0814622555..d97df3e322541 100644 --- a/packages/cli/test/integration/variables.test.ts +++ b/packages/cli/test/integration/variables.test.ts @@ -5,7 +5,6 @@ import * as testDb from './shared/testDb'; import * as utils from './shared/utils'; import type { AuthAgent } from './shared/types'; -import type { ClassLike, MockedClass } from 'jest-mock'; import { License } from '@/License'; // mock that credentialsSharing is not enabled @@ -14,7 +13,7 @@ let ownerUser: User; let memberUser: User; let authAgent: AuthAgent; let variablesSpy: jest.SpyInstance; -let licenseLike = { +const licenseLike = { isVariablesEnabled: jest.fn().mockReturnValue(true), getVariablesLimit: jest.fn().mockReturnValue(-1), }; diff --git a/packages/cli/test/integration/workflows.controller.test.ts b/packages/cli/test/integration/workflows.controller.test.ts index dd0d236ceb533..5c53cc25dba1d 100644 --- a/packages/cli/test/integration/workflows.controller.test.ts +++ b/packages/cli/test/integration/workflows.controller.test.ts @@ -1,4 +1,4 @@ -import { SuperAgentTest } from 'supertest'; +import type { SuperAgentTest } from 'supertest'; import type { IPinData } from 'n8n-workflow'; import type { User } from '@db/entities/User'; diff --git a/packages/cli/test/teardown.ts b/packages/cli/test/teardown.ts index bfc2748b1c303..07136f2ddb496 100644 --- a/packages/cli/test/teardown.ts +++ b/packages/cli/test/teardown.ts @@ -12,14 +12,14 @@ export default async () => { const query = dbType === 'postgres' ? 'SELECT datname as "Database" FROM pg_database' : 'SHOW DATABASES'; - const results: { Database: string }[] = await connection.query(query); + const results: Array<{ Database: string }> = await connection.query(query); const databases = results .filter( ({ Database: dbName }) => dbName.startsWith(`${dbType}_`) && dbName.endsWith('_n8n_test'), ) .map(({ Database: dbName }) => dbName); - const promises = databases.map((dbName) => connection.query(`DROP DATABASE ${dbName};`)); + const promises = databases.map(async (dbName) => connection.query(`DROP DATABASE ${dbName};`)); await Promise.all(promises); await connection.destroy(); }; diff --git a/packages/cli/test/unit/ActiveExecutions.test.ts b/packages/cli/test/unit/ActiveExecutions.test.ts index 3f27677052db8..c635fe7d299d1 100644 --- a/packages/cli/test/unit/ActiveExecutions.test.ts +++ b/packages/cli/test/unit/ActiveExecutions.test.ts @@ -3,13 +3,9 @@ import { ActiveExecutions } from '@/ActiveExecutions'; import { mocked } from 'jest-mock'; import PCancelable from 'p-cancelable'; import { v4 as uuid } from 'uuid'; -import { - createDeferredPromise, - IDeferredPromise, - IExecuteResponsePromiseData, - IRun, -} from 'n8n-workflow'; -import { IWorkflowExecutionDataProcess } from '@/Interfaces'; +import type { IDeferredPromise, IExecuteResponsePromiseData, IRun } from 'n8n-workflow'; +import { createDeferredPromise } from 'n8n-workflow'; +import type { IWorkflowExecutionDataProcess } from '@/Interfaces'; const FAKE_EXECUTION_ID = '15'; const FAKE_SECOND_EXECUTION_ID = '20'; @@ -160,12 +156,12 @@ function mockFullRunData(): IRun { }; } -function mockCancelablePromise(): PCancelable { +async function mockCancelablePromise(): PCancelable { return new PCancelable(async (resolve) => { resolve(); }); } -function mockDeferredPromise(): Promise> { +async function mockDeferredPromise(): Promise> { return createDeferredPromise(); } diff --git a/packages/cli/test/unit/ActiveWorkflowRunner.test.ts b/packages/cli/test/unit/ActiveWorkflowRunner.test.ts index ab1232cde6e62..c8694ceb88d31 100644 --- a/packages/cli/test/unit/ActiveWorkflowRunner.test.ts +++ b/packages/cli/test/unit/ActiveWorkflowRunner.test.ts @@ -1,13 +1,8 @@ import { v4 as uuid } from 'uuid'; import { mocked } from 'jest-mock'; -import { - ICredentialTypes, - INodesAndCredentials, - LoggerProxy, - NodeOperationError, - Workflow, -} from 'n8n-workflow'; +import type { ICredentialTypes, INodesAndCredentials } from 'n8n-workflow'; +import { LoggerProxy, NodeOperationError, Workflow } from 'n8n-workflow'; import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner'; import * as Db from '@/Db'; @@ -22,7 +17,7 @@ import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData' import { WorkflowRunner } from '@/WorkflowRunner'; import { mock } from 'jest-mock-extended'; -import { ExternalHooks } from '@/ExternalHooks'; +import type { ExternalHooks } from '@/ExternalHooks'; import { Container } from 'typedi'; import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; import { mockInstance } from '../integration/shared/utils'; diff --git a/packages/cli/test/unit/CommunityNodeHelpers.test.ts b/packages/cli/test/unit/CommunityNodeHelpers.test.ts index 6cb69d0085e2d..951a9f4b34b1a 100644 --- a/packages/cli/test/unit/CommunityNodeHelpers.test.ts +++ b/packages/cli/test/unit/CommunityNodeHelpers.test.ts @@ -145,7 +145,7 @@ describe('executeCommand', () => { ); }); - await expect(async () => await executeCommand('ls')).rejects.toThrow( + await expect(async () => executeCommand('ls')).rejects.toThrow( RESPONSE_ERROR_MESSAGES.PACKAGE_NOT_FOUND, ); diff --git a/packages/cli/test/unit/CredentialsHelper.test.ts b/packages/cli/test/unit/CredentialsHelper.test.ts index 8ede15e4fdcad..5a11254cdb916 100644 --- a/packages/cli/test/unit/CredentialsHelper.test.ts +++ b/packages/cli/test/unit/CredentialsHelper.test.ts @@ -1,4 +1,4 @@ -import { +import type { IAuthenticateGeneric, ICredentialDataDecryptedObject, ICredentialType, @@ -7,8 +7,9 @@ import { INode, INodeProperties, INodesAndCredentials, - Workflow, } from 'n8n-workflow'; +import { deepCopy } from 'n8n-workflow'; +import { Workflow } from 'n8n-workflow'; import { CredentialsHelper } from '@/CredentialsHelper'; import { CredentialTypes } from '@/CredentialTypes'; import { Container } from 'typedi'; @@ -82,7 +83,9 @@ describe('CredentialsHelper', () => { }, credentialType: new (class TestApi implements ICredentialType { name = 'testApi'; + displayName = 'Test API'; + properties: INodeProperties[] = [ { displayName: 'User', @@ -124,7 +127,9 @@ describe('CredentialsHelper', () => { }, credentialType: new (class TestApi implements ICredentialType { name = 'testApi'; + displayName = 'Test API'; + properties: INodeProperties[] = [ { displayName: 'Access Token', @@ -154,7 +159,9 @@ describe('CredentialsHelper', () => { }, credentialType: new (class TestApi implements ICredentialType { name = 'testApi'; + displayName = 'Test API'; + properties: INodeProperties[] = [ { displayName: 'Access Token', @@ -184,7 +191,9 @@ describe('CredentialsHelper', () => { }, credentialType: new (class TestApi implements ICredentialType { name = 'testApi'; + displayName = 'Test API'; + properties: INodeProperties[] = [ { displayName: 'Access Token', @@ -215,7 +224,9 @@ describe('CredentialsHelper', () => { }, credentialType: new (class TestApi implements ICredentialType { name = 'testApi'; + displayName = 'Test API'; + properties: INodeProperties[] = [ { displayName: 'My Token', @@ -229,8 +240,8 @@ describe('CredentialsHelper', () => { credentials: ICredentialDataDecryptedObject, requestOptions: IHttpRequestOptions, ): Promise { - requestOptions.headers!['Authorization'] = `Bearer ${credentials.accessToken}`; - requestOptions.qs!['user'] = credentials.user; + requestOptions.headers!.Authorization = `Bearer ${credentials.accessToken}`; + requestOptions.qs!.user = credentials.user; return requestOptions; } })(), @@ -287,7 +298,7 @@ describe('CredentialsHelper', () => { const result = await credentialsHelper.authenticate( testData.input.credentials, testData.input.credentialType.name, - JSON.parse(JSON.stringify(incomingRequestOptions)), + deepCopy(incomingRequestOptions), workflow, node, timezone, diff --git a/packages/cli/test/unit/CurlConverterHelper.test.ts b/packages/cli/test/unit/CurlConverterHelper.test.ts index dd492bfdca167..0f4ec60795624 100644 --- a/packages/cli/test/unit/CurlConverterHelper.test.ts +++ b/packages/cli/test/unit/CurlConverterHelper.test.ts @@ -17,7 +17,8 @@ describe('CurlConverterHelper', () => { }); test('Should parse JSON content type correctly', () => { - const curl = `curl -X POST https://reqbin.com/echo/post/json -H 'Content-Type: application/json' -d '{"login":"my_login","password":"my_password"}'`; + const curl = + 'curl -X POST https://reqbin.com/echo/post/json -H \'Content-Type: application/json\' -d \'{"login":"my_login","password":"my_password"}\''; const parameters = toHttpNodeParameters(curl); expect(parameters.url).toBe('https://reqbin.com/echo/post/json'); expect(parameters.sendBody).toBe(true); @@ -31,7 +32,8 @@ describe('CurlConverterHelper', () => { }); test('Should parse multipart-form-data content type correctly', () => { - const curl = `curl -X POST https://reqbin.com/echo/post/json -v -F key1=value1 -F upload=@localfilename`; + const curl = + 'curl -X POST https://reqbin.com/echo/post/json -v -F key1=value1 -F upload=@localfilename'; const parameters = toHttpNodeParameters(curl); expect(parameters.url).toBe('https://reqbin.com/echo/post/json'); expect(parameters.sendBody).toBe(true); @@ -46,7 +48,8 @@ describe('CurlConverterHelper', () => { }); test('Should parse binary request correctly', () => { - const curl = `curl --location --request POST 'https://www.website.com' --header 'Content-Type: image/png' --data-binary '@/Users/image.png`; + const curl = + "curl --location --request POST 'https://www.website.com' --header 'Content-Type: image/png' --data-binary '@/Users/image.png"; const parameters = toHttpNodeParameters(curl); expect(parameters.url).toBe('https://www.website.com'); expect(parameters.method).toBe('POST'); @@ -74,7 +77,8 @@ describe('CurlConverterHelper', () => { }); test('Should parse header properties and keep the original case', () => { - const curl = `curl -X POST https://reqbin.com/echo/post/json -v -F key1=value1 -F upload=@localfilename -H "ACCEPT: text/javascript" -H "content-type: multipart/form-data"`; + const curl = + 'curl -X POST https://reqbin.com/echo/post/json -v -F key1=value1 -F upload=@localfilename -H "ACCEPT: text/javascript" -H "content-type: multipart/form-data"'; const parameters = toHttpNodeParameters(curl); expect(parameters.url).toBe('https://reqbin.com/echo/post/json'); expect(parameters.sendBody).toBe(true); @@ -91,7 +95,7 @@ describe('CurlConverterHelper', () => { }); test('Should parse querystring properties', () => { - const curl = `curl -G -d 'q=kitties' -d 'count=20' https://google.com/search`; + const curl = "curl -G -d 'q=kitties' -d 'count=20' https://google.com/search"; const parameters = toHttpNodeParameters(curl); expect(parameters.url).toBe('https://google.com/search'); expect(parameters.sendBody).toBe(false); @@ -105,7 +109,7 @@ describe('CurlConverterHelper', () => { }); test('Should parse basic authentication property and keep the original case', () => { - const curl = `curl https://reqbin.com/echo -u "login:password"`; + const curl = 'curl https://reqbin.com/echo -u "login:password"'; const parameters = toHttpNodeParameters(curl); expect(parameters.url).toBe('https://reqbin.com/echo'); expect(parameters.sendBody).toBe(false); @@ -119,7 +123,7 @@ describe('CurlConverterHelper', () => { }); test('Should parse location flag with --location', () => { - const curl = `curl https://reqbin.com/echo -u "login:password" --location`; + const curl = 'curl https://reqbin.com/echo -u "login:password" --location'; const parameters = toHttpNodeParameters(curl); expect(parameters.url).toBe('https://reqbin.com/echo'); expect(parameters.sendBody).toBe(false); @@ -134,7 +138,7 @@ describe('CurlConverterHelper', () => { }); test('Should parse location flag with --L', () => { - const curl = `curl https://reqbin.com/echo -u "login:password" -L`; + const curl = 'curl https://reqbin.com/echo -u "login:password" -L'; const parameters = toHttpNodeParameters(curl); expect(parameters.url).toBe('https://reqbin.com/echo'); expect(parameters.sendBody).toBe(false); @@ -149,7 +153,7 @@ describe('CurlConverterHelper', () => { }); test('Should parse location and max redirects flags with --location and --max-redirs 10', () => { - const curl = `curl https://reqbin.com/echo -u "login:password" --location --max-redirs 10`; + const curl = 'curl https://reqbin.com/echo -u "login:password" --location --max-redirs 10'; const parameters = toHttpNodeParameters(curl); expect(parameters.url).toBe('https://reqbin.com/echo'); expect(parameters.sendBody).toBe(false); @@ -165,7 +169,7 @@ describe('CurlConverterHelper', () => { }); test('Should parse proxy flag -x', () => { - const curl = `curl https://reqbin.com/echo -u "login:password" -x https://google.com`; + const curl = 'curl https://reqbin.com/echo -u "login:password" -x https://google.com'; const parameters = toHttpNodeParameters(curl); expect(parameters.url).toBe('https://reqbin.com/echo'); expect(parameters.sendBody).toBe(false); @@ -180,7 +184,7 @@ describe('CurlConverterHelper', () => { }); test('Should parse proxy flag --proxy', () => { - const curl = `curl https://reqbin.com/echo -u "login:password" -x https://google.com`; + const curl = 'curl https://reqbin.com/echo -u "login:password" -x https://google.com'; const parameters = toHttpNodeParameters(curl); expect(parameters.url).toBe('https://reqbin.com/echo'); expect(parameters.sendBody).toBe(false); @@ -195,7 +199,7 @@ describe('CurlConverterHelper', () => { }); test('Should parse include headers on output flag --include', () => { - const curl = `curl https://reqbin.com/echo -u "login:password" --include -x https://google.com`; + const curl = 'curl https://reqbin.com/echo -u "login:password" --include -x https://google.com'; const parameters = toHttpNodeParameters(curl); expect(parameters.url).toBe('https://reqbin.com/echo'); expect(parameters.sendBody).toBe(false); @@ -210,7 +214,7 @@ describe('CurlConverterHelper', () => { }); test('Should parse include headers on output flag -i', () => { - const curl = `curl https://reqbin.com/echo -u "login:password" -x https://google.com -i`; + const curl = 'curl https://reqbin.com/echo -u "login:password" -x https://google.com -i'; const parameters = toHttpNodeParameters(curl); expect(parameters.url).toBe('https://reqbin.com/echo'); expect(parameters.sendBody).toBe(false); @@ -225,7 +229,7 @@ describe('CurlConverterHelper', () => { }); test('Should parse include request flag -X', () => { - const curl = `curl -X POST https://reqbin.com/echo -u "login:password" -x https://google.com`; + const curl = 'curl -X POST https://reqbin.com/echo -u "login:password" -x https://google.com'; const parameters = toHttpNodeParameters(curl); expect(parameters.url).toBe('https://reqbin.com/echo'); expect(parameters.method).toBe('POST'); @@ -233,7 +237,8 @@ describe('CurlConverterHelper', () => { }); test('Should parse include request flag --request', () => { - const curl = `curl --request POST https://reqbin.com/echo -u "login:password" -x https://google.com`; + const curl = + 'curl --request POST https://reqbin.com/echo -u "login:password" -x https://google.com'; const parameters = toHttpNodeParameters(curl); expect(parameters.url).toBe('https://reqbin.com/echo'); expect(parameters.method).toBe('POST'); @@ -241,7 +246,8 @@ describe('CurlConverterHelper', () => { }); test('Should parse include timeout flag --connect-timeout', () => { - const curl = `curl --request POST https://reqbin.com/echo -u "login:password" --connect-timeout 20`; + const curl = + 'curl --request POST https://reqbin.com/echo -u "login:password" --connect-timeout 20'; const parameters = toHttpNodeParameters(curl); expect(parameters.url).toBe('https://reqbin.com/echo'); expect(parameters.method).toBe('POST'); @@ -250,7 +256,7 @@ describe('CurlConverterHelper', () => { }); test('Should parse download file flag -O', () => { - const curl = `curl --request POST https://reqbin.com/echo -u "login:password" -O`; + const curl = 'curl --request POST https://reqbin.com/echo -u "login:password" -O'; const parameters = toHttpNodeParameters(curl); expect(parameters.url).toBe('https://reqbin.com/echo'); expect(parameters.method).toBe('POST'); @@ -260,7 +266,7 @@ describe('CurlConverterHelper', () => { }); test('Should parse download file flag -o', () => { - const curl = `curl --request POST https://reqbin.com/echo -u "login:password" -o`; + const curl = 'curl --request POST https://reqbin.com/echo -u "login:password" -o'; const parameters = toHttpNodeParameters(curl); expect(parameters.url).toBe('https://reqbin.com/echo'); expect(parameters.method).toBe('POST'); @@ -270,7 +276,7 @@ describe('CurlConverterHelper', () => { }); test('Should parse ignore SSL flag -k', () => { - const curl = `curl --request POST https://reqbin.com/echo -u "login:password" -k`; + const curl = 'curl --request POST https://reqbin.com/echo -u "login:password" -k'; const parameters = toHttpNodeParameters(curl); expect(parameters.url).toBe('https://reqbin.com/echo'); expect(parameters.method).toBe('POST'); @@ -279,7 +285,7 @@ describe('CurlConverterHelper', () => { }); test('Should parse ignore SSL flag --insecure', () => { - const curl = `curl --request POST https://reqbin.com/echo -u "login:password" --insecure`; + const curl = 'curl --request POST https://reqbin.com/echo -u "login:password" --insecure'; const parameters = toHttpNodeParameters(curl); expect(parameters.url).toBe('https://reqbin.com/echo'); expect(parameters.method).toBe('POST'); diff --git a/packages/cli/test/unit/Events.test.ts b/packages/cli/test/unit/Events.test.ts index d68aa19216389..ce9e3e58bd8f1 100644 --- a/packages/cli/test/unit/Events.test.ts +++ b/packages/cli/test/unit/Events.test.ts @@ -1,12 +1,13 @@ -import { IRun, LoggerProxy, WorkflowExecuteMode } from 'n8n-workflow'; +import type { IRun, WorkflowExecuteMode } from 'n8n-workflow'; +import { LoggerProxy } from 'n8n-workflow'; import { QueryFailedError } from 'typeorm'; import { mock } from 'jest-mock-extended'; import config from '@/config'; import * as Db from '@/Db'; import { User } from '@db/entities/User'; -import { WorkflowStatistics } from '@db/entities/WorkflowStatistics'; -import { WorkflowStatisticsRepository } from '@db/repositories'; +import type { WorkflowStatistics } from '@db/entities/WorkflowStatistics'; +import type { WorkflowStatisticsRepository } from '@db/repositories'; import { nodeFetchedData, workflowExecutionCompleted } from '@/events/WorkflowStatistics'; import * as UserManagementHelper from '@/UserManagement/UserManagementHelper'; import { getLogger } from '@/Logger'; diff --git a/packages/cli/test/unit/Helpers.ts b/packages/cli/test/unit/Helpers.ts index 2d9f43ebf598a..2504f1dfcf3f9 100644 --- a/packages/cli/test/unit/Helpers.ts +++ b/packages/cli/test/unit/Helpers.ts @@ -1,4 +1,4 @@ -import { INodeTypeData } from 'n8n-workflow'; +import type { INodeTypeData } from 'n8n-workflow'; /** * Ensure all pending promises settle. The promise's `resolve` is placed in @@ -29,7 +29,7 @@ export function mockNodeTypesData( outputs: [], properties: [], }, - trigger: options?.addTrigger ? () => Promise.resolve(undefined) : undefined, + trigger: options?.addTrigger ? async () => undefined : undefined, }, }), acc diff --git a/packages/cli/test/unit/License.test.ts b/packages/cli/test/unit/License.test.ts index 2c6c72d26ee14..32351f2e27aa6 100644 --- a/packages/cli/test/unit/License.test.ts +++ b/packages/cli/test/unit/License.test.ts @@ -10,7 +10,7 @@ const MOCK_RENEW_OFFSET = 259200; const MOCK_INSTANCE_ID = 'instance-id'; const MOCK_ACTIVATION_KEY = 'activation-key'; const MOCK_FEATURE_FLAG = 'feat:mock'; -const MOCK_MAIN_PLAN_ID = 1234; +const MOCK_MAIN_PLAN_ID = '1b765dc4-d39d-4ffe-9885-c56dd67c4b26'; describe('License', () => { beforeAll(() => { @@ -82,12 +82,21 @@ describe('License', () => { expect(LicenseManager.prototype.getManagementJwt).toHaveBeenCalled(); }); - test('check main plan', async () => { + test('getMainPlan() returns the right entitlement', async () => { // mock entitlements response License.prototype.getCurrentEntitlements = jest.fn().mockReturnValue([ + { + id: '84a9c852-1349-478d-9ad1-b3f55510e477', + productId: '670650f2-72d8-4397-898c-c249906e2cc2', + productMetadata: {}, + features: {}, + featureOverrides: {}, + validFrom: new Date(), + validTo: new Date(), + }, { id: MOCK_MAIN_PLAN_ID, - productId: '', + productId: '670650f2-72d8-4397-898c-c249906e2cc2', productMetadata: { terms: { isMainPlan: true, @@ -104,4 +113,32 @@ describe('License', () => { const mainPlan = license.getMainPlan(); expect(mainPlan?.id).toBe(MOCK_MAIN_PLAN_ID); }); + + test('getMainPlan() returns undefined if there is no main plan', async () => { + // mock entitlements response + License.prototype.getCurrentEntitlements = jest.fn().mockReturnValue([ + { + id: '84a9c852-1349-478d-9ad1-b3f55510e477', + productId: '670650f2-72d8-4397-898c-c249906e2cc2', + productMetadata: {}, // has no `productMetadata.terms.isMainPlan`! + features: {}, + featureOverrides: {}, + validFrom: new Date(), + validTo: new Date(), + }, + { + id: 'c1aae471-c24e-4874-ad88-b97107de486c', + productId: '670650f2-72d8-4397-898c-c249906e2cc2', + productMetadata: {}, // has no `productMetadata.terms.isMainPlan`! + features: {}, + featureOverrides: {}, + validFrom: new Date(), + validTo: new Date(), + }, + ]); + jest.fn(license.getMainPlan).mockReset(); + + const mainPlan = license.getMainPlan(); + expect(mainPlan).toBeUndefined(); + }); }); diff --git a/packages/cli/test/unit/PermissionChecker.test.ts b/packages/cli/test/unit/PermissionChecker.test.ts index 71b550c50fea5..c69bc597abce2 100644 --- a/packages/cli/test/unit/PermissionChecker.test.ts +++ b/packages/cli/test/unit/PermissionChecker.test.ts @@ -1,6 +1,7 @@ import { v4 as uuid } from 'uuid'; import { Container } from 'typedi'; -import { ICredentialTypes, INodeTypes, SubworkflowOperationError, Workflow } from 'n8n-workflow'; +import type { ICredentialTypes, INodeTypes } from 'n8n-workflow'; +import { SubworkflowOperationError, Workflow } from 'n8n-workflow'; import config from '@/config'; import * as Db from '@/Db'; @@ -79,7 +80,7 @@ describe('PermissionChecker.check()', () => { ], }); - expect(() => PermissionChecker.check(workflow, userId)).not.toThrow(); + expect(async () => PermissionChecker.check(workflow, userId)).not.toThrow(); }); test('should allow if requesting user is instance owner', async () => { @@ -109,7 +110,7 @@ describe('PermissionChecker.check()', () => { ], }); - expect(async () => await PermissionChecker.check(workflow, owner.id)).not.toThrow(); + expect(async () => PermissionChecker.check(workflow, owner.id)).not.toThrow(); }); test('should allow if workflow creds are valid subset', async () => { @@ -156,7 +157,7 @@ describe('PermissionChecker.check()', () => { ], }); - expect(async () => await PermissionChecker.check(workflow, owner.id)).not.toThrow(); + expect(async () => PermissionChecker.check(workflow, owner.id)).not.toThrow(); }); test('should deny if workflow creds are not valid subset', async () => { diff --git a/packages/cli/test/unit/PostHog.test.ts b/packages/cli/test/unit/PostHog.test.ts index 47e736014e6d9..978108c39c9ad 100644 --- a/packages/cli/test/unit/PostHog.test.ts +++ b/packages/cli/test/unit/PostHog.test.ts @@ -24,7 +24,7 @@ describe('PostHog', () => { const ph = new PostHogClient(); await ph.init(instanceId); - expect(PostHog.prototype.constructor).toHaveBeenCalledWith(apiKey, {host: apiHost}); + expect(PostHog.prototype.constructor).toHaveBeenCalledWith(apiKey, { host: apiHost }); }); it('does not initialize or track if diagnostics are not enabled', async () => { @@ -78,13 +78,10 @@ describe('PostHog', () => { createdAt, }); - expect(PostHog.prototype.getAllFlags).toHaveBeenCalledWith( - `${instanceId}#${userId}`, - { - personProperties: { - created_at_timestamp: createdAt.getTime().toString(), - }, - } - ); + expect(PostHog.prototype.getAllFlags).toHaveBeenCalledWith(`${instanceId}#${userId}`, { + personProperties: { + created_at_timestamp: createdAt.getTime().toString(), + }, + }); }); -}); \ No newline at end of file +}); diff --git a/packages/cli/test/unit/WorkflowCredentials.test.ts b/packages/cli/test/unit/WorkflowCredentials.test.ts index ece7dd66351b9..78afe95a643b5 100644 --- a/packages/cli/test/unit/WorkflowCredentials.test.ts +++ b/packages/cli/test/unit/WorkflowCredentials.test.ts @@ -12,16 +12,16 @@ async function mockFind({ type: string; }): Promise { // Simple statement that maps a return value based on the `id` parameter - if (id === notFoundNode.credentials!!.test.id) { + if (id === notFoundNode.credentials!.test.id) { return null; } // Otherwise just build some kind of credential object and return it return { [type]: { [id]: { - id: id, + id, name: type, - type: type, + type, nodesAccess: [], data: '', }, @@ -49,7 +49,7 @@ describe('WorkflowCredentials', () => { }); test('Should return an error if any node has no credential ID', () => { - const credentials = noIdNode.credentials!!.test; + const credentials = noIdNode.credentials!.test; const expectedError = new Error( `Credentials with name "${credentials.name}" for type "test" miss an ID.`, ); @@ -58,7 +58,7 @@ describe('WorkflowCredentials', () => { }); test('Should return an error if credentials cannot be found in the DB', () => { - const credentials = notFoundNode.credentials!!.test; + const credentials = notFoundNode.credentials!.test; const expectedError = new Error( `Could not find credentials for type "test" with ID "${credentials.id}".`, ); diff --git a/packages/cli/test/unit/WorkflowHelpers.test.ts b/packages/cli/test/unit/WorkflowHelpers.test.ts index 1f00375b63d3f..77cc85ec96bdd 100644 --- a/packages/cli/test/unit/WorkflowHelpers.test.ts +++ b/packages/cli/test/unit/WorkflowHelpers.test.ts @@ -1,4 +1,5 @@ -import { INode, LoggerProxy } from 'n8n-workflow'; +import type { INode } from 'n8n-workflow'; +import { LoggerProxy } from 'n8n-workflow'; import { WorkflowEntity } from '@db/entities/WorkflowEntity'; import { CredentialsEntity } from '@db/entities/CredentialsEntity'; import { getNodesWithInaccessibleCreds, validateWorkflowCredentialUsage } from '@/WorkflowHelpers'; diff --git a/packages/cli/test/unit/controllers/me.controller.test.ts b/packages/cli/test/unit/controllers/me.controller.test.ts index 20b8fdfb92d5e..8a622c67e4f62 100644 --- a/packages/cli/test/unit/controllers/me.controller.test.ts +++ b/packages/cli/test/unit/controllers/me.controller.test.ts @@ -1,11 +1,10 @@ -import { CookieOptions, Response } from 'express'; -import type { Repository } from 'typeorm'; +import type { CookieOptions, Response } from 'express'; import jwt from 'jsonwebtoken'; import { mock, anyObject, captor } from 'jest-mock-extended'; import type { ILogger } from 'n8n-workflow'; import type { IExternalHooksClass, IInternalHooksClass } from '@/Interfaces'; import type { User } from '@db/entities/User'; -import { UserRepository } from '@db/repositories'; +import type { UserRepository } from '@db/repositories'; import { MeController } from '@/controllers'; import { AUTH_COOKIE_NAME } from '@/constants'; import { BadRequestError } from '@/ResponseHelper'; diff --git a/packages/cli/test/unit/controllers/translation.controller.test.ts b/packages/cli/test/unit/controllers/translation.controller.test.ts index a24d462f81818..16e9645bbf44a 100644 --- a/packages/cli/test/unit/controllers/translation.controller.test.ts +++ b/packages/cli/test/unit/controllers/translation.controller.test.ts @@ -1,9 +1,9 @@ import { mock } from 'jest-mock-extended'; import type { ICredentialTypes } from 'n8n-workflow'; import type { Config } from '@/config'; +import type { TranslationRequest } from '@/controllers/translation.controller'; import { TranslationController, - TranslationRequest, CREDENTIAL_TRANSLATIONS_DIR, } from '@/controllers/translation.controller'; import { BadRequestError } from '@/ResponseHelper'; diff --git a/packages/cli/test/unit/repositories/role.repository.test.ts b/packages/cli/test/unit/repositories/role.repository.test.ts index ad37f797fc0c7..73dcc048b7d13 100644 --- a/packages/cli/test/unit/repositories/role.repository.test.ts +++ b/packages/cli/test/unit/repositories/role.repository.test.ts @@ -1,7 +1,8 @@ import { Container } from 'typedi'; import { DataSource, EntityManager } from 'typeorm'; import { mock } from 'jest-mock-extended'; -import { Role, RoleNames, RoleScopes } from '@db/entities/Role'; +import type { RoleNames, RoleScopes } from '@db/entities/Role'; +import { Role } from '@db/entities/Role'; import { RoleRepository } from '@db/repositories/role.repository'; import { mockInstance } from '../../integration/shared/utils'; import { randomInteger } from '../../integration/shared/random'; @@ -38,7 +39,7 @@ describe('RoleRepository', () => { test('should throw otherwise', async () => { entityManager.findOneOrFail.mockRejectedValueOnce(new Error()); - expect(() => roleRepository.findRoleOrFail('global', 'owner')).rejects.toThrow(); + expect(async () => roleRepository.findRoleOrFail('global', 'owner')).rejects.toThrow(); }); }); diff --git a/packages/cli/test/unit/utils.test.ts b/packages/cli/test/unit/utils.test.ts new file mode 100644 index 0000000000000..2925b4b66902d --- /dev/null +++ b/packages/cli/test/unit/utils.test.ts @@ -0,0 +1,35 @@ +import { webhookNotFoundErrorMessage } from '../../src/utils'; + +describe('utils test webhookNotFoundErrorMessage ', () => { + it('should return a message with path and method', () => { + const message = webhookNotFoundErrorMessage('webhook12345', 'GET'); + + expect(message).toEqual('The requested webhook "GET webhook12345" is not registered.'); + }); + it('should return a message with path', () => { + const message = webhookNotFoundErrorMessage('webhook12345'); + + expect(message).toEqual('The requested webhook "webhook12345" is not registered.'); + }); + it('should return a message with method with tip', () => { + const message = webhookNotFoundErrorMessage('webhook12345', 'POST', ['GET', 'PUT']); + + expect(message).toEqual( + 'This webhook is not registered for POST requests. Did you mean to make a GET or PUT request?', + ); + }); + it('should return a message with method with tip', () => { + const message = webhookNotFoundErrorMessage('webhook12345', 'POST', ['PUT']); + + expect(message).toEqual( + 'This webhook is not registered for POST requests. Did you mean to make a PUT request?', + ); + }); + it('should return a message with method with tip', () => { + const message = webhookNotFoundErrorMessage('webhook12345', 'POST', ['GET', 'PUT', 'DELETE']); + + expect(message).toEqual( + 'This webhook is not registered for POST requests. Did you mean to make a GET, PUT or DELETE request?', + ); + }); +}); diff --git a/packages/core/package.json b/packages/core/package.json index b1fc30e7a585c..699e12149c62b 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "n8n-core", - "version": "0.165.0", + "version": "0.166.0", "description": "Core functionality of n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", diff --git a/packages/core/src/WorkflowExecute.ts b/packages/core/src/WorkflowExecute.ts index 00e7a3f84f01c..072105c76a6e8 100644 --- a/packages/core/src/WorkflowExecute.ts +++ b/packages/core/src/WorkflowExecute.ts @@ -1286,12 +1286,16 @@ export class WorkflowExecute { message: executionError.message, stack: executionError.stack, } as ExecutionError; + if (executionError.message?.includes('canceled')) { + fullRunData.status = 'canceled'; + } } else if (this.runExecutionData.waitTill!) { // eslint-disable-next-line @typescript-eslint/restrict-template-expressions Logger.verbose(`Workflow execution will wait until ${this.runExecutionData.waitTill}`, { workflowId: workflow.id, }); fullRunData.waitTill = this.runExecutionData.waitTill; + fullRunData.status = 'waiting'; } else { Logger.verbose('Workflow execution finished successfully', { workflowId: workflow.id }); fullRunData.finished = true; @@ -1304,7 +1308,6 @@ export class WorkflowExecute { // Static data of workflow changed newStaticData = workflow.staticData; } - await this.executeHook('workflowExecuteAfter', [fullRunData, newStaticData]); if (closeFunction) { diff --git a/packages/core/test/Helpers.ts b/packages/core/test/Helpers.ts index c38362b8278a6..0d6874495483a 100644 --- a/packages/core/test/Helpers.ts +++ b/packages/core/test/Helpers.ts @@ -1,9 +1,7 @@ import set from 'lodash.set'; -import { +import type { ICredentialDataDecryptedObject, - ICredentialsHelper, - IDataObject, IDeferredPromise, IExecuteWorkflowInfo, IHttpRequestHelper, @@ -20,12 +18,12 @@ import { IVersionedNodeType, IWorkflowBase, IWorkflowExecuteAdditionalData, - NodeHelpers, NodeParameterValue, - WorkflowHooks, } from 'n8n-workflow'; +import { deepCopy } from 'n8n-workflow'; +import { ICredentialsHelper, NodeHelpers, WorkflowHooks } from 'n8n-workflow'; import { Credentials } from '@/Credentials'; -import { IExecuteFunctions } from '@/Interfaces'; +import type { IExecuteFunctions } from '@/Interfaces'; export class CredentialsHelper extends ICredentialsHelper { async authenticate( @@ -381,12 +379,12 @@ class NodeTypesClass implements INodeTypes { compareData.value2 as NodeParameterValue, ); - if (compareOperationResult === true && combineOperation === 'any') { + if (compareOperationResult && combineOperation === 'any') { // If it passes and the operation is "any" we do not have to check any // other ones as it should pass anyway. So go on with the next item. returnDataTrue.push(item); continue itemLoop; - } else if (compareOperationResult === false && combineOperation === 'all') { + } else if (!compareOperationResult && combineOperation === 'all') { // If it fails and the operation is "all" we do not have to check any // other ones as it should be not pass anyway. So go on with the next item. returnDataFalse.push(item); @@ -524,7 +522,7 @@ class NodeTypesClass implements INodeTypes { outputs: ['main'], properties: [], }, - execute(this: IExecuteFunctions): Promise { + async execute(this: IExecuteFunctions): Promise { const items = this.getInputData(); return this.prepareOutputData(items); }, @@ -570,7 +568,7 @@ class NodeTypesClass implements INodeTypes { }, ], }, - execute(this: IExecuteFunctions): Promise { + async execute(this: IExecuteFunctions): Promise { const items = this.getInputData(); const returnData: INodeExecutionData[] = []; @@ -702,13 +700,14 @@ class NodeTypesClass implements INodeTypes { name: 'dotNotation', type: 'boolean', default: true, - description: `

By default, dot-notation is used in property names. This means that "a.b" will set the property "b" underneath "a" so { "a": { "b": value} }.

If that is not intended this can be deactivated, it will then set { "a.b": value } instead.

`, + description: + '

By default, dot-notation is used in property names. This means that "a.b" will set the property "b" underneath "a" so { "a": { "b": value} }.

If that is not intended this can be deactivated, it will then set { "a.b": value } instead.

', }, ], }, ], }, - execute(this: IExecuteFunctions): Promise { + async execute(this: IExecuteFunctions): Promise { const items = this.getInputData(); if (items.length === 0) { @@ -722,13 +721,13 @@ class NodeTypesClass implements INodeTypes { for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { keepOnlySet = this.getNodeParameter('keepOnlySet', itemIndex, false) as boolean; item = items[itemIndex]; - const options = this.getNodeParameter('options', itemIndex, {}) as IDataObject; + const options = this.getNodeParameter('options', itemIndex, {}); const newItem: INodeExecutionData = { json: {}, }; - if (keepOnlySet !== true) { + if (!keepOnlySet) { if (item.binary !== undefined) { // Create a shallow copy of the binary data so that the old // data references which do not get changed still stay behind @@ -737,7 +736,7 @@ class NodeTypesClass implements INodeTypes { Object.assign(newItem.binary, item.binary); } - newItem.json = JSON.parse(JSON.stringify(item.json)); + newItem.json = deepCopy(item.json); } // Add boolean values @@ -797,7 +796,7 @@ class NodeTypesClass implements INodeTypes { outputs: ['main'], properties: [], }, - execute(this: IExecuteFunctions): Promise { + async execute(this: IExecuteFunctions): Promise { const items = this.getInputData(); return this.prepareOutputData(items); @@ -851,6 +850,8 @@ export function WorkflowExecuteAdditionalData( connections: {}, }; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore return { credentialsHelper: new CredentialsHelper(''), hooks: new WorkflowHooks(hookFunctions, 'trigger', '1', workflowData), diff --git a/packages/core/test/NodeExecuteFunctions.test.ts b/packages/core/test/NodeExecuteFunctions.test.ts index aff7ba036679b..f91c605b2cb3d 100644 --- a/packages/core/test/NodeExecuteFunctions.test.ts +++ b/packages/core/test/NodeExecuteFunctions.test.ts @@ -28,7 +28,7 @@ describe('NodeExecuteFunctions', () => { BinaryDataManager.instance = undefined; }); - test(`test getBinaryDataBuffer(...) & setBinaryDataBuffer(...) methods in 'default' mode`, async () => { + test("test getBinaryDataBuffer(...) & setBinaryDataBuffer(...) methods in 'default' mode", async () => { // Setup a 'default' binary data manager instance await BinaryDataManager.init({ mode: 'default', @@ -39,8 +39,8 @@ describe('NodeExecuteFunctions', () => { }); // Set our binary data buffer - let inputData: Buffer = Buffer.from('This is some binary data', 'utf8'); - let setBinaryDataBufferResponse: IBinaryData = await setBinaryDataBuffer( + const inputData: Buffer = Buffer.from('This is some binary data', 'utf8'); + const setBinaryDataBufferResponse: IBinaryData = await setBinaryDataBuffer( { mimeType: 'txt', data: 'This should be overwritten by the actual payload in the response', @@ -54,7 +54,7 @@ describe('NodeExecuteFunctions', () => { // Now, re-fetch our data. // An ITaskDataConnections object is used to share data between nodes. The top level property, 'main', represents the successful output object from a previous node. - let taskDataConnectionsInput: ITaskDataConnections = { + const taskDataConnectionsInput: ITaskDataConnections = { main: [], }; @@ -69,7 +69,7 @@ describe('NodeExecuteFunctions', () => { ]); // Now, lets fetch our data! The item will be item index 0. - let getBinaryDataBufferResponse: Buffer = await getBinaryDataBuffer( + const getBinaryDataBufferResponse: Buffer = await getBinaryDataBuffer( taskDataConnectionsInput, 0, 'data', @@ -79,7 +79,7 @@ describe('NodeExecuteFunctions', () => { expect(getBinaryDataBufferResponse).toEqual(inputData); }); - test(`test getBinaryDataBuffer(...) & setBinaryDataBuffer(...) methods in 'filesystem' mode`, async () => { + test("test getBinaryDataBuffer(...) & setBinaryDataBuffer(...) methods in 'filesystem' mode", async () => { // Setup a 'filesystem' binary data manager instance await BinaryDataManager.init({ mode: 'filesystem', @@ -90,8 +90,8 @@ describe('NodeExecuteFunctions', () => { }); // Set our binary data buffer - let inputData: Buffer = Buffer.from('This is some binary data', 'utf8'); - let setBinaryDataBufferResponse: IBinaryData = await setBinaryDataBuffer( + const inputData: Buffer = Buffer.from('This is some binary data', 'utf8'); + const setBinaryDataBufferResponse: IBinaryData = await setBinaryDataBuffer( { mimeType: 'txt', data: 'This should be overwritten with the name of the configured data manager', @@ -112,7 +112,7 @@ describe('NodeExecuteFunctions', () => { // Now, re-fetch our data. // An ITaskDataConnections object is used to share data between nodes. The top level property, 'main', represents the successful output object from a previous node. - let taskDataConnectionsInput: ITaskDataConnections = { + const taskDataConnectionsInput: ITaskDataConnections = { main: [], }; @@ -127,7 +127,7 @@ describe('NodeExecuteFunctions', () => { ]); // Now, lets fetch our data! The item will be item index 0. - let getBinaryDataBufferResponse: Buffer = await getBinaryDataBuffer( + const getBinaryDataBufferResponse: Buffer = await getBinaryDataBuffer( taskDataConnectionsInput, 0, 'data', diff --git a/packages/core/test/WorkflowExecute.test.ts b/packages/core/test/WorkflowExecute.test.ts index 5a79bac72339f..5c5eb2ba58826 100644 --- a/packages/core/test/WorkflowExecute.test.ts +++ b/packages/core/test/WorkflowExecute.test.ts @@ -1,4 +1,5 @@ -import { createDeferredPromise, IConnections, INode, IRun, Workflow } from 'n8n-workflow'; +import type { IConnections, INode, IRun } from 'n8n-workflow'; +import { createDeferredPromise, Workflow } from 'n8n-workflow'; import { WorkflowExecute } from '@/WorkflowExecute'; import * as Helpers from './Helpers'; diff --git a/packages/core/test/utils.ts b/packages/core/test/utils.ts index 1d6abbf50efaa..ca7c804dca44a 100644 --- a/packages/core/test/utils.ts +++ b/packages/core/test/utils.ts @@ -1,4 +1,5 @@ -import { ILogger, LoggerProxy } from 'n8n-workflow'; +import type { ILogger } from 'n8n-workflow'; +import { LoggerProxy } from 'n8n-workflow'; const fakeLogger = { log: () => {}, diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json index 38a3666755a13..de946ffa6e6c8 100644 --- a/packages/editor-ui/package.json +++ b/packages/editor-ui/package.json @@ -1,6 +1,6 @@ { "name": "n8n-editor-ui", - "version": "0.192.0", + "version": "0.193.0", "description": "Workflow Editor UI for n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", diff --git a/packages/editor-ui/src/components/ParameterInput.vue b/packages/editor-ui/src/components/ParameterInput.vue index acbc2898ffdf6..565e8bf13933f 100644 --- a/packages/editor-ui/src/components/ParameterInput.vue +++ b/packages/editor-ui/src/components/ParameterInput.vue @@ -402,6 +402,7 @@ import { useNDVStore } from '@/stores/ndv'; import { useNodeTypesStore } from '@/stores/nodeTypes'; import { useCredentialsStore } from '@/stores/credentials'; import { htmlEditorEventBus } from '@/event-bus'; +import Vue from 'vue'; type ResourceLocatorRef = InstanceType; @@ -988,8 +989,7 @@ export default mixins( this.nodeName = this.node.name; } - // Set focus on field - setTimeout(() => { + Vue.nextTick(() => { // @ts-ignore if (this.$refs.inputField?.focus && this.$refs.inputField?.$el) { // @ts-ignore diff --git a/packages/editor-ui/src/components/ParameterInputHint.vue b/packages/editor-ui/src/components/ParameterInputHint.vue index fbbd176d784c3..d683e23537f60 100644 --- a/packages/editor-ui/src/components/ParameterInputHint.vue +++ b/packages/editor-ui/src/components/ParameterInputHint.vue @@ -1,11 +1,6 @@