Compare commits
2 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
96544df7d5 | ||
![]() |
036db63a33 |
@@ -5,11 +5,8 @@ BACKEND_PORT=3000
|
|||||||
WEB_PORT=3001
|
WEB_PORT=3001
|
||||||
|
|
||||||
echo "Configuring backend environment variables..."
|
echo "Configuring backend environment variables..."
|
||||||
|
|
||||||
cd packages/backend
|
cd packages/backend
|
||||||
|
|
||||||
rm -rf .env
|
rm -rf .env
|
||||||
|
|
||||||
echo "
|
echo "
|
||||||
PORT=$BACKEND_PORT
|
PORT=$BACKEND_PORT
|
||||||
WEB_APP_URL=http://localhost:$WEB_PORT
|
WEB_APP_URL=http://localhost:$WEB_PORT
|
||||||
@@ -24,34 +21,23 @@ WEBHOOK_SECRET_KEY=sample_webhook_secret_key
|
|||||||
APP_SECRET_KEY=sample_app_secret_key
|
APP_SECRET_KEY=sample_app_secret_key
|
||||||
REDIS_HOST=redis
|
REDIS_HOST=redis
|
||||||
SERVE_WEB_APP_SEPARATELY=true" >> .env
|
SERVE_WEB_APP_SEPARATELY=true" >> .env
|
||||||
|
|
||||||
echo "Installing backend dependencies..."
|
|
||||||
|
|
||||||
yarn
|
|
||||||
|
|
||||||
cd $CURRENT_DIR
|
cd $CURRENT_DIR
|
||||||
|
|
||||||
echo "Configuring web environment variables..."
|
echo "Configuring web environment variables..."
|
||||||
|
|
||||||
cd packages/web
|
cd packages/web
|
||||||
|
|
||||||
rm -rf .env
|
rm -rf .env
|
||||||
|
|
||||||
echo "
|
echo "
|
||||||
PORT=$WEB_PORT
|
PORT=$WEB_PORT
|
||||||
REACT_APP_BACKEND_URL=http://localhost:$BACKEND_PORT
|
REACT_APP_BACKEND_URL=http://localhost:$BACKEND_PORT
|
||||||
" >> .env
|
" >> .env
|
||||||
|
|
||||||
echo "Installing web dependencies..."
|
|
||||||
|
|
||||||
yarn
|
|
||||||
|
|
||||||
cd $CURRENT_DIR
|
cd $CURRENT_DIR
|
||||||
|
|
||||||
|
echo "Installing and linking dependencies..."
|
||||||
|
yarn
|
||||||
|
yarn lerna bootstrap
|
||||||
|
|
||||||
echo "Migrating database..."
|
echo "Migrating database..."
|
||||||
|
|
||||||
cd packages/backend
|
cd packages/backend
|
||||||
|
|
||||||
yarn db:migrate
|
yarn db:migrate
|
||||||
yarn db:seed:user
|
yarn db:seed:user
|
||||||
|
|
||||||
|
9
.github/workflows/backend.yml
vendored
9
.github/workflows/backend.yml
vendored
@@ -41,11 +41,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 18
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: yarn
|
run: cd packages/backend && yarn
|
||||||
working-directory: packages/backend
|
|
||||||
- name: Copy .env-example.test file to .env.test
|
- name: Copy .env-example.test file to .env.test
|
||||||
run: cp .env-example.test .env.test
|
run: cd packages/backend && cp .env-example.test .env.test
|
||||||
working-directory: packages/backend
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: yarn test:coverage
|
run: cd packages/backend && yarn test
|
||||||
working-directory: packages/backend
|
|
||||||
|
30
.github/workflows/ci.yml
vendored
30
.github/workflows/ci.yml
vendored
@@ -18,13 +18,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '18'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
cache-dependency-path: packages/backend/yarn.lock
|
cache-dependency-path: yarn.lock
|
||||||
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
|
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
|
||||||
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
|
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
|
||||||
- run: yarn --frozen-lockfile
|
- run: yarn --frozen-lockfile
|
||||||
working-directory: packages/backend
|
- run: cd packages/backend && yarn lint
|
||||||
- run: yarn lint
|
|
||||||
working-directory: packages/backend
|
|
||||||
- run: echo "🍏 This job's status is ${{ job.status }}."
|
- run: echo "🍏 This job's status is ${{ job.status }}."
|
||||||
start-backend-server:
|
start-backend-server:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -37,13 +35,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '18'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
cache-dependency-path: packages/backend/yarn.lock
|
cache-dependency-path: yarn.lock
|
||||||
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
|
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
|
||||||
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
|
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
|
||||||
- run: yarn --frozen-lockfile
|
- run: yarn --frozen-lockfile && yarn lerna bootstrap
|
||||||
working-directory: packages/backend
|
- run: cd packages/backend && yarn start
|
||||||
- run: yarn start
|
|
||||||
working-directory: packages/backend
|
|
||||||
env:
|
env:
|
||||||
ENCRYPTION_KEY: sample_encryption_key
|
ENCRYPTION_KEY: sample_encryption_key
|
||||||
WEBHOOK_SECRET_KEY: sample_webhook_secret_key
|
WEBHOOK_SECRET_KEY: sample_webhook_secret_key
|
||||||
@@ -59,13 +55,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '18'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
cache-dependency-path: packages/backend/yarn.lock
|
cache-dependency-path: yarn.lock
|
||||||
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
|
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
|
||||||
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
|
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
|
||||||
- run: yarn --frozen-lockfile
|
- run: yarn --frozen-lockfile && yarn lerna bootstrap
|
||||||
working-directory: packages/backend
|
- run: cd packages/backend && yarn start:worker
|
||||||
- run: yarn start:worker
|
|
||||||
working-directory: packages/backend
|
|
||||||
env:
|
env:
|
||||||
ENCRYPTION_KEY: sample_encryption_key
|
ENCRYPTION_KEY: sample_encryption_key
|
||||||
WEBHOOK_SECRET_KEY: sample_webhook_secret_key
|
WEBHOOK_SECRET_KEY: sample_webhook_secret_key
|
||||||
@@ -81,13 +75,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '18'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
cache-dependency-path: packages/web/yarn.lock
|
cache-dependency-path: yarn.lock
|
||||||
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
|
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
|
||||||
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
|
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
|
||||||
- run: yarn --frozen-lockfile
|
- run: yarn --frozen-lockfile && yarn lerna bootstrap
|
||||||
working-directory: packages/web
|
- run: cd packages/web && yarn build
|
||||||
- run: yarn build
|
|
||||||
working-directory: packages/web
|
|
||||||
env:
|
env:
|
||||||
CI: false
|
CI: false
|
||||||
- run: echo "🍏 This job's status is ${{ job.status }}."
|
- run: echo "🍏 This job's status is ${{ job.status }}."
|
||||||
|
15
.github/workflows/playwright.yml
vendored
15
.github/workflows/playwright.yml
vendored
@@ -3,7 +3,6 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
# TODO: Add pull request after optimizing the total excecution time of the test suite.
|
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- 'packages/backend/**'
|
- 'packages/backend/**'
|
||||||
@@ -59,21 +58,13 @@ jobs:
|
|||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 18
|
||||||
- name: Install web dependencies
|
- name: Install dependencies
|
||||||
run: yarn
|
run: yarn && yarn lerna bootstrap
|
||||||
working-directory: ./packages/web
|
|
||||||
- name: Install backend dependencies
|
|
||||||
run: yarn
|
|
||||||
working-directory: ./packages/backend
|
|
||||||
- name: Install e2e-tests dependencies
|
|
||||||
run: yarn
|
|
||||||
working-directory: ./packages/e2e-tests
|
|
||||||
- name: Install Playwright Browsers
|
- name: Install Playwright Browsers
|
||||||
run: yarn playwright install --with-deps
|
run: yarn playwright install --with-deps
|
||||||
working-directory: ./packages/e2e-tests
|
|
||||||
- name: Build Automatisch web
|
- name: Build Automatisch web
|
||||||
run: yarn build
|
|
||||||
working-directory: ./packages/web
|
working-directory: ./packages/web
|
||||||
|
run: yarn build
|
||||||
env:
|
env:
|
||||||
# Keep this until we clean up warnings in build processes
|
# Keep this until we clean up warnings in build processes
|
||||||
CI: false
|
CI: false
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,6 +4,7 @@ logs
|
|||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
.pnpm-debug.log*
|
.pnpm-debug.log*
|
||||||
|
|
||||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
@@ -11,12 +11,10 @@ WORKDIR /automatisch
|
|||||||
# copy the app, note .dockerignore
|
# copy the app, note .dockerignore
|
||||||
COPY . /automatisch
|
COPY . /automatisch
|
||||||
|
|
||||||
RUN cd packages/web && yarn
|
RUN yarn
|
||||||
|
|
||||||
RUN cd packages/web && yarn build
|
RUN cd packages/web && yarn build
|
||||||
|
|
||||||
RUN cd packages/backend && yarn --production
|
|
||||||
|
|
||||||
RUN \
|
RUN \
|
||||||
rm -rf /usr/local/share/.cache/ && \
|
rm -rf /usr/local/share/.cache/ && \
|
||||||
apk del build-dependencies
|
apk del build-dependencies
|
||||||
|
13
lerna.json
Normal file
13
lerna.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"packages": [
|
||||||
|
"packages/*"
|
||||||
|
],
|
||||||
|
"version": "0.10.0",
|
||||||
|
"npmClient": "yarn",
|
||||||
|
"useWorkspaces": true,
|
||||||
|
"command": {
|
||||||
|
"add": {
|
||||||
|
"exact": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
32
package.json
Normal file
32
package.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "@automatisch/root",
|
||||||
|
"license": "See LICENSE file",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"start": "lerna run --stream --parallel --scope=@*/{web,backend} dev",
|
||||||
|
"start:web": "lerna run --stream --scope=@*/web dev",
|
||||||
|
"start:backend": "lerna run --stream --scope=@*/backend dev",
|
||||||
|
"build:docs": "cd ./packages/docs && yarn install && yarn build"
|
||||||
|
},
|
||||||
|
"workspaces": {
|
||||||
|
"packages": [
|
||||||
|
"packages/*"
|
||||||
|
],
|
||||||
|
"nohoist": [
|
||||||
|
"**/babel-loader",
|
||||||
|
"**/webpack",
|
||||||
|
"**/@automatisch/web",
|
||||||
|
"**/ajv"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"eslint": "^8.13.0",
|
||||||
|
"eslint-config-prettier": "^8.3.0",
|
||||||
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
|
"lerna": "^4.0.0",
|
||||||
|
"prettier": "^2.5.1"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
}
|
||||||
|
}
|
@@ -12,7 +12,6 @@
|
|||||||
"pretest": "APP_ENV=test node ./test/setup/prepare-test-env.js",
|
"pretest": "APP_ENV=test node ./test/setup/prepare-test-env.js",
|
||||||
"test": "APP_ENV=test vitest run",
|
"test": "APP_ENV=test vitest run",
|
||||||
"test:watch": "APP_ENV=test vitest watch",
|
"test:watch": "APP_ENV=test vitest watch",
|
||||||
"test:coverage": "yarn test --coverage",
|
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"db:create": "node ./bin/database/create.js",
|
"db:create": "node ./bin/database/create.js",
|
||||||
"db:seed:user": "node ./bin/database/seed-user.js",
|
"db:seed:user": "node ./bin/database/seed-user.js",
|
||||||
@@ -24,7 +23,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bull-board/express": "^3.10.1",
|
"@bull-board/express": "^3.10.1",
|
||||||
"@casl/ability": "^6.5.0",
|
"@casl/ability": "^6.5.0",
|
||||||
"@faker-js/faker": "^9.2.0",
|
|
||||||
"@node-saml/passport-saml": "^4.0.4",
|
"@node-saml/passport-saml": "^4.0.4",
|
||||||
"@rudderstack/rudder-sdk-node": "^1.1.2",
|
"@rudderstack/rudder-sdk-node": "^1.1.2",
|
||||||
"@sentry/node": "^7.42.0",
|
"@sentry/node": "^7.42.0",
|
||||||
@@ -38,9 +36,6 @@
|
|||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
"debug": "~2.6.9",
|
"debug": "~2.6.9",
|
||||||
"dotenv": "^10.0.0",
|
"dotenv": "^10.0.0",
|
||||||
"eslint": "^8.13.0",
|
|
||||||
"eslint-config-prettier": "^8.3.0",
|
|
||||||
"eslint-plugin-prettier": "^4.0.0",
|
|
||||||
"express": "~4.18.2",
|
"express": "~4.18.2",
|
||||||
"express-async-errors": "^3.1.1",
|
"express-async-errors": "^3.1.1",
|
||||||
"express-basic-auth": "^1.2.1",
|
"express-basic-auth": "^1.2.1",
|
||||||
@@ -66,7 +61,6 @@
|
|||||||
"pg": "^8.7.1",
|
"pg": "^8.7.1",
|
||||||
"php-serialize": "^4.0.2",
|
"php-serialize": "^4.0.2",
|
||||||
"pluralize": "^8.0.0",
|
"pluralize": "^8.0.0",
|
||||||
"prettier": "^2.5.1",
|
|
||||||
"raw-body": "^2.5.2",
|
"raw-body": "^2.5.2",
|
||||||
"showdown": "^2.1.0",
|
"showdown": "^2.1.0",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
@@ -98,11 +92,10 @@
|
|||||||
"url": "https://github.com/automatisch/automatisch/issues"
|
"url": "https://github.com/automatisch/automatisch/issues"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitest/coverage-v8": "^2.1.5",
|
|
||||||
"node-gyp": "^10.1.0",
|
"node-gyp": "^10.1.0",
|
||||||
"nodemon": "^2.0.13",
|
"nodemon": "^2.0.13",
|
||||||
"supertest": "^6.3.3",
|
"supertest": "^6.3.3",
|
||||||
"vitest": "^2.1.5"
|
"vitest": "^1.1.3"
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
|
@@ -8,7 +8,7 @@ export default {
|
|||||||
key: 'instanceUrl',
|
key: 'instanceUrl',
|
||||||
label: 'WordPress instance URL',
|
label: 'WordPress instance URL',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
required: true,
|
required: false,
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
value: null,
|
value: null,
|
||||||
placeholder: null,
|
placeholder: null,
|
||||||
|
@@ -52,7 +52,7 @@ const appConfig = {
|
|||||||
isDev: appEnv === 'development',
|
isDev: appEnv === 'development',
|
||||||
isTest: appEnv === 'test',
|
isTest: appEnv === 'test',
|
||||||
isProd: appEnv === 'production',
|
isProd: appEnv === 'production',
|
||||||
version: '0.14.0',
|
version: '0.13.1',
|
||||||
postgresDatabase: process.env.POSTGRES_DATABASE || 'automatisch_development',
|
postgresDatabase: process.env.POSTGRES_DATABASE || 'automatisch_development',
|
||||||
postgresSchema: process.env.POSTGRES_SCHEMA || 'public',
|
postgresSchema: process.env.POSTGRES_SCHEMA || 'public',
|
||||||
postgresPort: parseInt(process.env.POSTGRES_PORT || '5432'),
|
postgresPort: parseInt(process.env.POSTGRES_PORT || '5432'),
|
||||||
|
@@ -10,11 +10,12 @@ export default async (request, response) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const appConfigParams = (request) => {
|
const appConfigParams = (request) => {
|
||||||
const { useOnlyPredefinedAuthClients, disabled } = request.body;
|
const { customConnectionAllowed, shared, disabled } = request.body;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
key: request.params.appKey,
|
key: request.params.appKey,
|
||||||
useOnlyPredefinedAuthClients,
|
customConnectionAllowed,
|
||||||
|
shared,
|
||||||
disabled,
|
disabled,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@@ -23,7 +23,8 @@ describe('POST /api/v1/admin/apps/:appKey/config', () => {
|
|||||||
|
|
||||||
it('should return created app config', async () => {
|
it('should return created app config', async () => {
|
||||||
const appConfig = {
|
const appConfig = {
|
||||||
useOnlyPredefinedAuthClients: false,
|
customConnectionAllowed: true,
|
||||||
|
shared: true,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -37,14 +38,14 @@ describe('POST /api/v1/admin/apps/:appKey/config', () => {
|
|||||||
...appConfig,
|
...appConfig,
|
||||||
key: 'gitlab',
|
key: 'gitlab',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(response.body).toMatchObject(expectedPayload);
|
expect(response.body).toMatchObject(expectedPayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return HTTP 422 for already existing app config', async () => {
|
it('should return HTTP 422 for already existing app config', async () => {
|
||||||
const appConfig = {
|
const appConfig = {
|
||||||
key: 'gitlab',
|
key: 'gitlab',
|
||||||
useOnlyPredefinedAuthClients: false,
|
customConnectionAllowed: true,
|
||||||
|
shared: true,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -17,10 +17,11 @@ export default async (request, response) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const appConfigParams = (request) => {
|
const appConfigParams = (request) => {
|
||||||
const { useOnlyPredefinedAuthClients, disabled } = request.body;
|
const { customConnectionAllowed, shared, disabled } = request.body;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
useOnlyPredefinedAuthClients,
|
customConnectionAllowed,
|
||||||
|
shared,
|
||||||
disabled,
|
disabled,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@@ -24,15 +24,17 @@ describe('PATCH /api/v1/admin/apps/:appKey/config', () => {
|
|||||||
it('should return updated app config', async () => {
|
it('should return updated app config', async () => {
|
||||||
const appConfig = {
|
const appConfig = {
|
||||||
key: 'gitlab',
|
key: 'gitlab',
|
||||||
useOnlyPredefinedAuthClients: true,
|
customConnectionAllowed: true,
|
||||||
|
shared: true,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
await createAppConfig(appConfig);
|
await createAppConfig(appConfig);
|
||||||
|
|
||||||
const newAppConfigValues = {
|
const newAppConfigValues = {
|
||||||
|
shared: false,
|
||||||
disabled: true,
|
disabled: true,
|
||||||
useOnlyPredefinedAuthClients: false,
|
customConnectionAllowed: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await request(app)
|
const response = await request(app)
|
||||||
@@ -51,8 +53,9 @@ describe('PATCH /api/v1/admin/apps/:appKey/config', () => {
|
|||||||
|
|
||||||
it('should return not found response for unexisting app config', async () => {
|
it('should return not found response for unexisting app config', async () => {
|
||||||
const appConfig = {
|
const appConfig = {
|
||||||
|
shared: false,
|
||||||
disabled: true,
|
disabled: true,
|
||||||
useOnlyPredefinedAuthClients: false,
|
customConnectionAllowed: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
await request(app)
|
await request(app)
|
||||||
@@ -65,7 +68,8 @@ describe('PATCH /api/v1/admin/apps/:appKey/config', () => {
|
|||||||
it('should return HTTP 422 for invalid app config data', async () => {
|
it('should return HTTP 422 for invalid app config data', async () => {
|
||||||
const appConfig = {
|
const appConfig = {
|
||||||
key: 'gitlab',
|
key: 'gitlab',
|
||||||
useOnlyPredefinedAuthClients: true,
|
customConnectionAllowed: true,
|
||||||
|
shared: true,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -7,7 +7,7 @@ export default async (request, response) => {
|
|||||||
.throwIfNotFound();
|
.throwIfNotFound();
|
||||||
|
|
||||||
const roleMappings = await samlAuthProvider
|
const roleMappings = await samlAuthProvider
|
||||||
.$relatedQuery('roleMappings')
|
.$relatedQuery('samlAuthProvidersRoleMappings')
|
||||||
.orderBy('remote_role_name', 'asc');
|
.orderBy('remote_role_name', 'asc');
|
||||||
|
|
||||||
renderObject(response, roleMappings);
|
renderObject(response, roleMappings);
|
||||||
|
@@ -8,14 +8,15 @@ export default async (request, response) => {
|
|||||||
.findById(samlAuthProviderId)
|
.findById(samlAuthProviderId)
|
||||||
.throwIfNotFound();
|
.throwIfNotFound();
|
||||||
|
|
||||||
const roleMappings = await samlAuthProvider.updateRoleMappings(
|
const samlAuthProvidersRoleMappings =
|
||||||
roleMappingsParams(request)
|
await samlAuthProvider.updateRoleMappings(
|
||||||
);
|
samlAuthProvidersRoleMappingsParams(request)
|
||||||
|
);
|
||||||
|
|
||||||
renderObject(response, roleMappings);
|
renderObject(response, samlAuthProvidersRoleMappings);
|
||||||
};
|
};
|
||||||
|
|
||||||
const roleMappingsParams = (request) => {
|
const samlAuthProvidersRoleMappingsParams = (request) => {
|
||||||
const roleMappings = request.body;
|
const roleMappings = request.body;
|
||||||
|
|
||||||
return roleMappings.map(({ roleId, remoteRoleName }) => ({
|
return roleMappings.map(({ roleId, remoteRoleName }) => ({
|
||||||
|
@@ -6,7 +6,7 @@ import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by
|
|||||||
import { createRole } from '../../../../../../test/factories/role.js';
|
import { createRole } from '../../../../../../test/factories/role.js';
|
||||||
import { createUser } from '../../../../../../test/factories/user.js';
|
import { createUser } from '../../../../../../test/factories/user.js';
|
||||||
import { createSamlAuthProvider } from '../../../../../../test/factories/saml-auth-provider.ee.js';
|
import { createSamlAuthProvider } from '../../../../../../test/factories/saml-auth-provider.ee.js';
|
||||||
import { createRoleMapping } from '../../../../../../test/factories/role-mapping.js';
|
import { createSamlAuthProvidersRoleMapping } from '../../../../../../test/factories/saml-auth-providers-role-mapping.js';
|
||||||
import createRoleMappingsMock from '../../../../../../test/mocks/rest/api/v1/admin/saml-auth-providers/update-role-mappings.ee.js';
|
import createRoleMappingsMock from '../../../../../../test/mocks/rest/api/v1/admin/saml-auth-providers/update-role-mappings.ee.js';
|
||||||
import * as license from '../../../../../helpers/license.ee.js';
|
import * as license from '../../../../../helpers/license.ee.js';
|
||||||
|
|
||||||
@@ -21,12 +21,12 @@ describe('PATCH /api/v1/admin/saml-auth-providers/:samlAuthProviderId/role-mappi
|
|||||||
|
|
||||||
samlAuthProvider = await createSamlAuthProvider();
|
samlAuthProvider = await createSamlAuthProvider();
|
||||||
|
|
||||||
await createRoleMapping({
|
await createSamlAuthProvidersRoleMapping({
|
||||||
samlAuthProviderId: samlAuthProvider.id,
|
samlAuthProviderId: samlAuthProvider.id,
|
||||||
remoteRoleName: 'Viewer',
|
remoteRoleName: 'Viewer',
|
||||||
});
|
});
|
||||||
|
|
||||||
await createRoleMapping({
|
await createSamlAuthProvidersRoleMapping({
|
||||||
samlAuthProviderId: samlAuthProvider.id,
|
samlAuthProviderId: samlAuthProvider.id,
|
||||||
remoteRoleName: 'Editor',
|
remoteRoleName: 'Editor',
|
||||||
});
|
});
|
||||||
@@ -64,7 +64,7 @@ describe('PATCH /api/v1/admin/saml-auth-providers/:samlAuthProviderId/role-mappi
|
|||||||
|
|
||||||
it('should delete role mappings when given empty role mappings', async () => {
|
it('should delete role mappings when given empty role mappings', async () => {
|
||||||
const existingRoleMappings = await samlAuthProvider.$relatedQuery(
|
const existingRoleMappings = await samlAuthProvider.$relatedQuery(
|
||||||
'roleMappings'
|
'samlAuthProvidersRoleMappings'
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(existingRoleMappings.length).toBe(2);
|
expect(existingRoleMappings.length).toBe(2);
|
||||||
@@ -149,4 +149,34 @@ describe('PATCH /api/v1/admin/saml-auth-providers/:samlAuthProviderId/role-mappi
|
|||||||
.send(roleMappings)
|
.send(roleMappings)
|
||||||
.expect(404);
|
.expect(404);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not delete existing role mapping when error thrown', async () => {
|
||||||
|
const roleMappings = [
|
||||||
|
{
|
||||||
|
roleId: userRole.id,
|
||||||
|
remoteRoleName: {
|
||||||
|
invalid: 'data',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const roleMappingsBeforeRequest = await samlAuthProvider.$relatedQuery(
|
||||||
|
'samlAuthProvidersRoleMappings'
|
||||||
|
);
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.patch(
|
||||||
|
`/api/v1/admin/saml-auth-providers/${samlAuthProvider.id}/role-mappings`
|
||||||
|
)
|
||||||
|
.set('Authorization', token)
|
||||||
|
.send(roleMappings)
|
||||||
|
.expect(422);
|
||||||
|
|
||||||
|
const roleMappingsAfterRequest = await samlAuthProvider.$relatedQuery(
|
||||||
|
'samlAuthProvidersRoleMappings'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(roleMappingsBeforeRequest).toStrictEqual(roleMappingsAfterRequest);
|
||||||
|
expect(roleMappingsAfterRequest.length).toBe(2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -155,7 +155,7 @@ describe('POST /api/v1/apps/:appKey/connections', () => {
|
|||||||
await createAppConfig({
|
await createAppConfig({
|
||||||
key: 'gitlab',
|
key: 'gitlab',
|
||||||
disabled: false,
|
disabled: false,
|
||||||
useOnlyPredefinedAuthClients: false,
|
customConnectionAllowed: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -218,7 +218,7 @@ describe('POST /api/v1/apps/:appKey/connections', () => {
|
|||||||
await createAppConfig({
|
await createAppConfig({
|
||||||
key: 'gitlab',
|
key: 'gitlab',
|
||||||
disabled: false,
|
disabled: false,
|
||||||
useOnlyPredefinedAuthClients: true,
|
customConnectionAllowed: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -266,14 +266,14 @@ describe('POST /api/v1/apps/:appKey/connections', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('with auth client enabled', async () => {
|
describe('with auth clients enabled', async () => {
|
||||||
let appAuthClient;
|
let appAuthClient;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await createAppConfig({
|
await createAppConfig({
|
||||||
key: 'gitlab',
|
key: 'gitlab',
|
||||||
disabled: false,
|
disabled: false,
|
||||||
useOnlyPredefinedAuthClients: false,
|
shared: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
appAuthClient = await createAppAuthClient({
|
appAuthClient = await createAppAuthClient({
|
||||||
@@ -310,6 +310,19 @@ describe('POST /api/v1/apps/:appKey/connections', () => {
|
|||||||
expect(response.body).toStrictEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return not authorized response for appAuthClientId and formattedData together', async () => {
|
||||||
|
const connectionData = {
|
||||||
|
appAuthClientId: appAuthClient.id,
|
||||||
|
formattedData: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
await request(app)
|
||||||
|
.post('/api/v1/apps/gitlab/connections')
|
||||||
|
.set('Authorization', token)
|
||||||
|
.send(connectionData)
|
||||||
|
.expect(403);
|
||||||
|
});
|
||||||
|
|
||||||
it('should return not found response for invalid app key', async () => {
|
it('should return not found response for invalid app key', async () => {
|
||||||
await request(app)
|
await request(app)
|
||||||
.post('/api/v1/apps/invalid-app-key/connections')
|
.post('/api/v1/apps/invalid-app-key/connections')
|
||||||
@@ -336,20 +349,18 @@ describe('POST /api/v1/apps/:appKey/connections', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('with auth clients disabled', async () => {
|
||||||
describe('with auth client disabled', async () => {
|
|
||||||
let appAuthClient;
|
let appAuthClient;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await createAppConfig({
|
await createAppConfig({
|
||||||
key: 'gitlab',
|
key: 'gitlab',
|
||||||
disabled: false,
|
disabled: false,
|
||||||
useOnlyPredefinedAuthClients: false,
|
shared: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
appAuthClient = await createAppAuthClient({
|
appAuthClient = await createAppAuthClient({
|
||||||
appKey: 'gitlab',
|
appKey: 'gitlab',
|
||||||
active: false,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -362,7 +373,7 @@ describe('POST /api/v1/apps/:appKey/connections', () => {
|
|||||||
.post('/api/v1/apps/gitlab/connections')
|
.post('/api/v1/apps/gitlab/connections')
|
||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.send(connectionData)
|
.send(connectionData)
|
||||||
.expect(404);
|
.expect(403);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return not found response for invalid app key', async () => {
|
it('should return not found response for invalid app key', async () => {
|
||||||
|
@@ -17,7 +17,8 @@ describe('GET /api/v1/apps/:appKey/config', () => {
|
|||||||
|
|
||||||
appConfig = await createAppConfig({
|
appConfig = await createAppConfig({
|
||||||
key: 'deepl',
|
key: 'deepl',
|
||||||
useOnlyPredefinedAuthClients: false,
|
customConnectionAllowed: true,
|
||||||
|
shared: true,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -87,14 +87,14 @@ describe('GET /api/v1/apps/:appKey/connections', () => {
|
|||||||
|
|
||||||
it('should return not found response for invalid connection UUID', async () => {
|
it('should return not found response for invalid connection UUID', async () => {
|
||||||
await createPermission({
|
await createPermission({
|
||||||
action: 'read',
|
action: 'update',
|
||||||
subject: 'Connection',
|
subject: 'Connection',
|
||||||
roleId: currentUserRole.id,
|
roleId: currentUserRole.id,
|
||||||
conditions: ['isCreator'],
|
conditions: ['isCreator'],
|
||||||
});
|
});
|
||||||
|
|
||||||
await request(app)
|
await request(app)
|
||||||
.get('/api/v1/apps/invalid-connection-id/connections')
|
.get('/api/v1/connections/invalid-connection-id/connections')
|
||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.expect(404);
|
.expect(404);
|
||||||
});
|
});
|
||||||
|
@@ -10,7 +10,7 @@ describe('GET /api/v1/automatisch/version', () => {
|
|||||||
|
|
||||||
const expectedPayload = {
|
const expectedPayload = {
|
||||||
data: {
|
data: {
|
||||||
version: '0.14.0',
|
version: '0.13.1',
|
||||||
},
|
},
|
||||||
meta: {
|
meta: {
|
||||||
count: 1,
|
count: 1,
|
||||||
|
@@ -47,6 +47,7 @@ describe('POST /api/v1/connections/:connectionId/reset', () => {
|
|||||||
|
|
||||||
const expectedPayload = resetConnectionMock({
|
const expectedPayload = resetConnectionMock({
|
||||||
...refetchedCurrentUserConnection,
|
...refetchedCurrentUserConnection,
|
||||||
|
reconnectable: refetchedCurrentUserConnection.reconnectable,
|
||||||
formattedData: {
|
formattedData: {
|
||||||
screenName: 'Connection name',
|
screenName: 'Connection name',
|
||||||
},
|
},
|
||||||
|
@@ -55,9 +55,10 @@ describe('PATCH /api/v1/connections/:connectionId', () => {
|
|||||||
|
|
||||||
const refetchedCurrentUserConnection = await currentUserConnection.$query();
|
const refetchedCurrentUserConnection = await currentUserConnection.$query();
|
||||||
|
|
||||||
const expectedPayload = updateConnectionMock(
|
const expectedPayload = updateConnectionMock({
|
||||||
refetchedCurrentUserConnection
|
...refetchedCurrentUserConnection,
|
||||||
);
|
reconnectable: refetchedCurrentUserConnection.reconnectable,
|
||||||
|
});
|
||||||
|
|
||||||
expect(response.body).toStrictEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
import { renderObject } from '../../../../helpers/renderer.js';
|
import { renderObject } from '../../../../helpers/renderer.js';
|
||||||
|
|
||||||
export default async (request, response) => {
|
export default async (request, response) => {
|
||||||
const flow = await request.currentUser.$relatedQuery('flows').insertAndFetch({
|
let flow = await request.currentUser.$relatedQuery('flows').insert({
|
||||||
name: 'Name your flow',
|
name: 'Name your flow',
|
||||||
});
|
});
|
||||||
|
|
||||||
await flow.createInitialSteps();
|
flow = await flow.createInitialSteps();
|
||||||
|
|
||||||
renderObject(response, flow, { status: 201 });
|
renderObject(response, flow, { status: 201 });
|
||||||
};
|
};
|
||||||
|
@@ -6,7 +6,7 @@ export default async (request, response) => {
|
|||||||
.findById(request.params.flowId)
|
.findById(request.params.flowId)
|
||||||
.throwIfNotFound();
|
.throwIfNotFound();
|
||||||
|
|
||||||
const createdActionStep = await flow.createStepAfter(
|
const createdActionStep = await flow.createActionStep(
|
||||||
request.body.previousStepId
|
request.body.previousStepId
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@@ -193,7 +193,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-data', () => {
|
|||||||
const notExistingStepUUID = Crypto.randomUUID();
|
const notExistingStepUUID = Crypto.randomUUID();
|
||||||
|
|
||||||
await request(app)
|
await request(app)
|
||||||
.post(`/api/v1/steps/${notExistingStepUUID}/dynamic-data`)
|
.get(`/api/v1/steps/${notExistingStepUUID}/dynamic-data`)
|
||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.expect(404);
|
.expect(404);
|
||||||
});
|
});
|
||||||
@@ -216,7 +216,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-data', () => {
|
|||||||
const step = await createStep({ appKey: null });
|
const step = await createStep({ appKey: null });
|
||||||
|
|
||||||
await request(app)
|
await request(app)
|
||||||
.post(`/api/v1/steps/${step.id}/dynamic-data`)
|
.get(`/api/v1/steps/${step.id}/dynamic-data`)
|
||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.expect(404);
|
.expect(404);
|
||||||
});
|
});
|
||||||
|
@@ -118,7 +118,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-fields', () => {
|
|||||||
const notExistingStepUUID = Crypto.randomUUID();
|
const notExistingStepUUID = Crypto.randomUUID();
|
||||||
|
|
||||||
await request(app)
|
await request(app)
|
||||||
.post(`/api/v1/steps/${notExistingStepUUID}/dynamic-fields`)
|
.get(`/api/v1/steps/${notExistingStepUUID}/dynamic-fields`)
|
||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.expect(404);
|
.expect(404);
|
||||||
});
|
});
|
||||||
@@ -138,11 +138,10 @@ describe('POST /api/v1/steps/:stepId/dynamic-fields', () => {
|
|||||||
conditions: [],
|
conditions: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const step = await createStep();
|
const step = await createStep({ appKey: null });
|
||||||
await step.$query().patch({ appKey: null });
|
|
||||||
|
|
||||||
await request(app)
|
await request(app)
|
||||||
.post(`/api/v1/steps/${step.id}/dynamic-fields`)
|
.get(`/api/v1/steps/${step.id}/dynamic-fields`)
|
||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.expect(404);
|
.expect(404);
|
||||||
});
|
});
|
||||||
|
@@ -1,52 +0,0 @@
|
|||||||
export async function up(knex) {
|
|
||||||
await knex.schema.createTable('role_mappings', (table) => {
|
|
||||||
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
|
|
||||||
table
|
|
||||||
.uuid('saml_auth_provider_id')
|
|
||||||
.references('id')
|
|
||||||
.inTable('saml_auth_providers');
|
|
||||||
table.uuid('role_id').references('id').inTable('roles');
|
|
||||||
table.string('remote_role_name').notNullable();
|
|
||||||
|
|
||||||
table.unique(['saml_auth_provider_id', 'remote_role_name']);
|
|
||||||
|
|
||||||
table.timestamps(true, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
const existingRoleMappings = await knex('saml_auth_providers_role_mappings');
|
|
||||||
|
|
||||||
if (existingRoleMappings.length) {
|
|
||||||
await knex('role_mappings').insert(existingRoleMappings);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await knex.schema.dropTable('saml_auth_providers_role_mappings');
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(knex) {
|
|
||||||
await knex.schema.createTable(
|
|
||||||
'saml_auth_providers_role_mappings',
|
|
||||||
(table) => {
|
|
||||||
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
|
|
||||||
table
|
|
||||||
.uuid('saml_auth_provider_id')
|
|
||||||
.references('id')
|
|
||||||
.inTable('saml_auth_providers');
|
|
||||||
table.uuid('role_id').references('id').inTable('roles');
|
|
||||||
table.string('remote_role_name').notNullable();
|
|
||||||
|
|
||||||
table.unique(['saml_auth_provider_id', 'remote_role_name']);
|
|
||||||
|
|
||||||
table.timestamps(true, true);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const existingRoleMappings = await knex('role_mappings');
|
|
||||||
|
|
||||||
if (existingRoleMappings.length) {
|
|
||||||
await knex('saml_auth_providers_role_mappings').insert(
|
|
||||||
existingRoleMappings
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await knex.schema.dropTable('role_mappings');
|
|
||||||
}
|
|
@@ -1,11 +0,0 @@
|
|||||||
export async function up(knex) {
|
|
||||||
return await knex.schema.alterTable('app_configs', (table) => {
|
|
||||||
table.boolean('use_only_predefined_auth_clients').defaultTo(false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(knex) {
|
|
||||||
return await knex.schema.alterTable('app_configs', (table) => {
|
|
||||||
table.dropColumn('use_only_predefined_auth_clients');
|
|
||||||
});
|
|
||||||
}
|
|
@@ -1,15 +0,0 @@
|
|||||||
export async function up(knex) {
|
|
||||||
return await knex.schema.alterTable('app_configs', (table) => {
|
|
||||||
table.dropColumn('shared');
|
|
||||||
table.dropColumn('connection_allowed');
|
|
||||||
table.dropColumn('custom_connection_allowed');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function down(knex) {
|
|
||||||
return await knex.schema.alterTable('app_configs', (table) => {
|
|
||||||
table.boolean('shared').defaultTo(false);
|
|
||||||
table.boolean('connection_allowed').defaultTo(false);
|
|
||||||
table.boolean('custom_connection_allowed').defaultTo(false);
|
|
||||||
});
|
|
||||||
}
|
|
@@ -30,7 +30,7 @@ const findOrCreateUserBySamlIdentity = async (
|
|||||||
: [mappedUser.role];
|
: [mappedUser.role];
|
||||||
|
|
||||||
const samlAuthProviderRoleMapping = await samlAuthProvider
|
const samlAuthProviderRoleMapping = await samlAuthProvider
|
||||||
.$relatedQuery('roleMappings')
|
.$relatedQuery('samlAuthProvidersRoleMappings')
|
||||||
.whereIn('remote_role_name', mappedRoles)
|
.whereIn('remote_role_name', mappedRoles)
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.first();
|
.first();
|
||||||
|
@@ -1,46 +0,0 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
|
||||||
import userAbility from './user-ability.js';
|
|
||||||
|
|
||||||
describe('userAbility', () => {
|
|
||||||
it('should return PureAbility instantiated with user permissions', () => {
|
|
||||||
const user = {
|
|
||||||
permissions: [
|
|
||||||
{
|
|
||||||
subject: 'Flow',
|
|
||||||
action: 'read',
|
|
||||||
conditions: ['isCreator'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
role: {
|
|
||||||
name: 'User',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const ability = userAbility(user);
|
|
||||||
|
|
||||||
expect(ability.rules).toStrictEqual(user.permissions);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return permission-less PureAbility for user with no role', () => {
|
|
||||||
const user = {
|
|
||||||
permissions: [
|
|
||||||
{
|
|
||||||
subject: 'Flow',
|
|
||||||
action: 'read',
|
|
||||||
conditions: ['isCreator'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
role: null,
|
|
||||||
};
|
|
||||||
const ability = userAbility(user);
|
|
||||||
|
|
||||||
expect(ability.rules).toStrictEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return permission-less PureAbility for user with no permissions', () => {
|
|
||||||
const user = { permissions: null, role: { name: 'User' } };
|
|
||||||
const ability = userAbility(user);
|
|
||||||
|
|
||||||
expect(ability.rules).toStrictEqual([]);
|
|
||||||
});
|
|
||||||
});
|
|
@@ -3,9 +3,17 @@
|
|||||||
exports[`AppConfig model > jsonSchema should have correct validations 1`] = `
|
exports[`AppConfig model > jsonSchema should have correct validations 1`] = `
|
||||||
{
|
{
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"connectionAllowed": {
|
||||||
|
"default": false,
|
||||||
|
"type": "boolean",
|
||||||
|
},
|
||||||
"createdAt": {
|
"createdAt": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
},
|
},
|
||||||
|
"customConnectionAllowed": {
|
||||||
|
"default": false,
|
||||||
|
"type": "boolean",
|
||||||
|
},
|
||||||
"disabled": {
|
"disabled": {
|
||||||
"default": false,
|
"default": false,
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
@@ -17,13 +25,13 @@ exports[`AppConfig model > jsonSchema should have correct validations 1`] = `
|
|||||||
"key": {
|
"key": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
},
|
},
|
||||||
"updatedAt": {
|
"shared": {
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"useOnlyPredefinedAuthClients": {
|
|
||||||
"default": false,
|
"default": false,
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
},
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"key",
|
"key",
|
||||||
|
@@ -1,42 +0,0 @@
|
|||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
||||||
|
|
||||||
exports[`Flow model > jsonSchema should have correct validations 1`] = `
|
|
||||||
{
|
|
||||||
"properties": {
|
|
||||||
"active": {
|
|
||||||
"type": "boolean",
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"deletedAt": {
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"id": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"minLength": 1,
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"publishedAt": {
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"remoteWebhookId": {
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"userId": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"name",
|
|
||||||
],
|
|
||||||
"type": "object",
|
|
||||||
}
|
|
||||||
`;
|
|
@@ -1,30 +0,0 @@
|
|||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
||||||
|
|
||||||
exports[`RoleMapping model > jsonSchema should have the correct schema 1`] = `
|
|
||||||
{
|
|
||||||
"properties": {
|
|
||||||
"id": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"remoteRoleName": {
|
|
||||||
"minLength": 1,
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"roleId": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"samlAuthProviderId": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"samlAuthProviderId",
|
|
||||||
"roleId",
|
|
||||||
"remoteRoleName",
|
|
||||||
],
|
|
||||||
"type": "object",
|
|
||||||
}
|
|
||||||
`;
|
|
@@ -1,72 +0,0 @@
|
|||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
||||||
|
|
||||||
exports[`SamlAuthProvider model > jsonSchema should have the correct schema 1`] = `
|
|
||||||
{
|
|
||||||
"properties": {
|
|
||||||
"active": {
|
|
||||||
"type": "boolean",
|
|
||||||
},
|
|
||||||
"certificate": {
|
|
||||||
"minLength": 1,
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"defaultRoleId": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"emailAttributeName": {
|
|
||||||
"minLength": 1,
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"entryPoint": {
|
|
||||||
"minLength": 1,
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"firstnameAttributeName": {
|
|
||||||
"minLength": 1,
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"id": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"issuer": {
|
|
||||||
"minLength": 1,
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"minLength": 1,
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"roleAttributeName": {
|
|
||||||
"minLength": 1,
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"signatureAlgorithm": {
|
|
||||||
"enum": [
|
|
||||||
"sha1",
|
|
||||||
"sha256",
|
|
||||||
"sha512",
|
|
||||||
],
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"surnameAttributeName": {
|
|
||||||
"minLength": 1,
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"name",
|
|
||||||
"certificate",
|
|
||||||
"signatureAlgorithm",
|
|
||||||
"entryPoint",
|
|
||||||
"issuer",
|
|
||||||
"firstnameAttributeName",
|
|
||||||
"surnameAttributeName",
|
|
||||||
"emailAttributeName",
|
|
||||||
"roleAttributeName",
|
|
||||||
"defaultRoleId",
|
|
||||||
],
|
|
||||||
"type": "object",
|
|
||||||
}
|
|
||||||
`;
|
|
@@ -1,6 +1,6 @@
|
|||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
exports[`RoleMapping model > jsonSchema should have the correct schema 1`] = `
|
exports[`SamlAuthProvidersRoleMapping model > jsonSchema should have the correct schema 1`] = `
|
||||||
{
|
{
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"id": {
|
||||||
@@ -28,3 +28,14 @@ exports[`RoleMapping model > jsonSchema should have the correct schema 1`] = `
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`SamlAuthProvidersRoleMapping model > relationMappings should have samlAuthProvider relation 1`] = `
|
||||||
|
{
|
||||||
|
"join": {
|
||||||
|
"from": "saml_auth_providers_role_mappings.saml_auth_provider_id",
|
||||||
|
"to": "saml_auth_providers.id",
|
||||||
|
},
|
||||||
|
"modelClass": [Function],
|
||||||
|
"relation": [Function],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
@@ -1,77 +0,0 @@
|
|||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
||||||
|
|
||||||
exports[`Step model > jsonSchema should have correct validations 1`] = `
|
|
||||||
{
|
|
||||||
"properties": {
|
|
||||||
"appKey": {
|
|
||||||
"maxLength": 255,
|
|
||||||
"minLength": 1,
|
|
||||||
"type": [
|
|
||||||
"string",
|
|
||||||
"null",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"connectionId": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": [
|
|
||||||
"string",
|
|
||||||
"null",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"deletedAt": {
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"flowId": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"id": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"key": {
|
|
||||||
"type": [
|
|
||||||
"string",
|
|
||||||
"null",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
},
|
|
||||||
"position": {
|
|
||||||
"type": "integer",
|
|
||||||
},
|
|
||||||
"status": {
|
|
||||||
"default": "incomplete",
|
|
||||||
"enum": [
|
|
||||||
"incomplete",
|
|
||||||
"completed",
|
|
||||||
],
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"enum": [
|
|
||||||
"action",
|
|
||||||
"trigger",
|
|
||||||
],
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"webhookPath": {
|
|
||||||
"type": [
|
|
||||||
"string",
|
|
||||||
"null",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"type",
|
|
||||||
],
|
|
||||||
"type": "object",
|
|
||||||
}
|
|
||||||
`;
|
|
@@ -1,81 +0,0 @@
|
|||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
||||||
|
|
||||||
exports[`User model > jsonSchema should have correct validations 1`] = `
|
|
||||||
{
|
|
||||||
"properties": {
|
|
||||||
"createdAt": {
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"deletedAt": {
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"email": {
|
|
||||||
"format": "email",
|
|
||||||
"maxLength": 255,
|
|
||||||
"minLength": 1,
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"fullName": {
|
|
||||||
"minLength": 1,
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"id": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"invitationToken": {
|
|
||||||
"type": [
|
|
||||||
"string",
|
|
||||||
"null",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"invitationTokenSentAt": {
|
|
||||||
"format": "date-time",
|
|
||||||
"type": [
|
|
||||||
"string",
|
|
||||||
"null",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"password": {
|
|
||||||
"minLength": 6,
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"resetPasswordToken": {
|
|
||||||
"type": [
|
|
||||||
"string",
|
|
||||||
"null",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"resetPasswordTokenSentAt": {
|
|
||||||
"format": "date-time",
|
|
||||||
"type": [
|
|
||||||
"string",
|
|
||||||
"null",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"roleId": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"status": {
|
|
||||||
"default": "active",
|
|
||||||
"enum": [
|
|
||||||
"active",
|
|
||||||
"invited",
|
|
||||||
],
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"trialExpiryDate": {
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"fullName",
|
|
||||||
"email",
|
|
||||||
],
|
|
||||||
"type": "object",
|
|
||||||
}
|
|
||||||
`;
|
|
@@ -60,26 +60,39 @@ class AppAuthClient extends Base {
|
|||||||
return this.authDefaults ? true : false;
|
return this.authDefaults ? true : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async triggerAppConfigUpdate() {
|
||||||
|
const appConfig = await this.$relatedQuery('appConfig');
|
||||||
|
|
||||||
|
// This is a workaround to update connection allowed column for AppConfig
|
||||||
|
await appConfig?.$query().patch({
|
||||||
|
key: appConfig.key,
|
||||||
|
shared: appConfig.shared,
|
||||||
|
disabled: appConfig.disabled,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Make another abstraction like beforeSave instead of using
|
// TODO: Make another abstraction like beforeSave instead of using
|
||||||
// beforeInsert and beforeUpdate separately for the same operation.
|
// beforeInsert and beforeUpdate separately for the same operation.
|
||||||
async $beforeInsert(queryContext) {
|
async $beforeInsert(queryContext) {
|
||||||
await super.$beforeInsert(queryContext);
|
await super.$beforeInsert(queryContext);
|
||||||
|
|
||||||
this.encryptData();
|
this.encryptData();
|
||||||
}
|
}
|
||||||
|
|
||||||
async $afterInsert(queryContext) {
|
async $afterInsert(queryContext) {
|
||||||
await super.$afterInsert(queryContext);
|
await super.$afterInsert(queryContext);
|
||||||
|
|
||||||
|
await this.triggerAppConfigUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
async $beforeUpdate(opt, queryContext) {
|
async $beforeUpdate(opt, queryContext) {
|
||||||
await super.$beforeUpdate(opt, queryContext);
|
await super.$beforeUpdate(opt, queryContext);
|
||||||
|
|
||||||
this.encryptData();
|
this.encryptData();
|
||||||
}
|
}
|
||||||
|
|
||||||
async $afterUpdate(opt, queryContext) {
|
async $afterUpdate(opt, queryContext) {
|
||||||
await super.$afterUpdate(opt, queryContext);
|
await super.$afterUpdate(opt, queryContext);
|
||||||
|
|
||||||
|
await this.triggerAppConfigUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
async $afterFind() {
|
async $afterFind() {
|
||||||
|
@@ -7,6 +7,7 @@ import AppAuthClient from './app-auth-client.js';
|
|||||||
import Base from './base.js';
|
import Base from './base.js';
|
||||||
import appConfig from '../config/app.js';
|
import appConfig from '../config/app.js';
|
||||||
import { createAppAuthClient } from '../../test/factories/app-auth-client.js';
|
import { createAppAuthClient } from '../../test/factories/app-auth-client.js';
|
||||||
|
import { createAppConfig } from '../../test/factories/app-config.js';
|
||||||
|
|
||||||
describe('AppAuthClient model', () => {
|
describe('AppAuthClient model', () => {
|
||||||
it('tableName should return correct name', () => {
|
it('tableName should return correct name', () => {
|
||||||
@@ -163,6 +164,63 @@ describe('AppAuthClient model', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('triggerAppConfigUpdate', () => {
|
||||||
|
it('should trigger an update in related app config', async () => {
|
||||||
|
await createAppConfig({ key: 'gitlab' });
|
||||||
|
|
||||||
|
const appAuthClient = await createAppAuthClient({
|
||||||
|
appKey: 'gitlab',
|
||||||
|
});
|
||||||
|
|
||||||
|
const appConfigBeforeUpdateSpy = vi.spyOn(
|
||||||
|
AppConfig.prototype,
|
||||||
|
'$beforeUpdate'
|
||||||
|
);
|
||||||
|
|
||||||
|
await appAuthClient.triggerAppConfigUpdate();
|
||||||
|
|
||||||
|
expect(appConfigBeforeUpdateSpy).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update related AppConfig after creating an instance', async () => {
|
||||||
|
const appConfig = await createAppConfig({
|
||||||
|
key: 'gitlab',
|
||||||
|
disabled: false,
|
||||||
|
shared: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await createAppAuthClient({
|
||||||
|
appKey: 'gitlab',
|
||||||
|
active: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const refetchedAppConfig = await appConfig.$query();
|
||||||
|
|
||||||
|
expect(refetchedAppConfig.connectionAllowed).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update related AppConfig after updating an instance', async () => {
|
||||||
|
const appConfig = await createAppConfig({
|
||||||
|
key: 'gitlab',
|
||||||
|
disabled: false,
|
||||||
|
shared: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const appAuthClient = await createAppAuthClient({
|
||||||
|
appKey: 'gitlab',
|
||||||
|
active: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
let refetchedAppConfig = await appConfig.$query();
|
||||||
|
expect(refetchedAppConfig.connectionAllowed).toBe(false);
|
||||||
|
|
||||||
|
await appAuthClient.$query().patchAndFetch({ active: true });
|
||||||
|
|
||||||
|
refetchedAppConfig = await appConfig.$query();
|
||||||
|
expect(refetchedAppConfig.connectionAllowed).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('$beforeInsert should call AppAuthClient.encryptData', async () => {
|
it('$beforeInsert should call AppAuthClient.encryptData', async () => {
|
||||||
const appAuthClientBeforeInsertSpy = vi.spyOn(
|
const appAuthClientBeforeInsertSpy = vi.spyOn(
|
||||||
AppAuthClient.prototype,
|
AppAuthClient.prototype,
|
||||||
@@ -174,6 +232,17 @@ describe('AppAuthClient model', () => {
|
|||||||
expect(appAuthClientBeforeInsertSpy).toHaveBeenCalledOnce();
|
expect(appAuthClientBeforeInsertSpy).toHaveBeenCalledOnce();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('$afterInsert should call AppAuthClient.triggerAppConfigUpdate', async () => {
|
||||||
|
const appAuthClientAfterInsertSpy = vi.spyOn(
|
||||||
|
AppAuthClient.prototype,
|
||||||
|
'triggerAppConfigUpdate'
|
||||||
|
);
|
||||||
|
|
||||||
|
await createAppAuthClient();
|
||||||
|
|
||||||
|
expect(appAuthClientAfterInsertSpy).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
it('$beforeUpdate should call AppAuthClient.encryptData', async () => {
|
it('$beforeUpdate should call AppAuthClient.encryptData', async () => {
|
||||||
const appAuthClient = await createAppAuthClient();
|
const appAuthClient = await createAppAuthClient();
|
||||||
|
|
||||||
@@ -187,6 +256,19 @@ describe('AppAuthClient model', () => {
|
|||||||
expect(appAuthClientBeforeUpdateSpy).toHaveBeenCalledOnce();
|
expect(appAuthClientBeforeUpdateSpy).toHaveBeenCalledOnce();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('$afterUpdate should call AppAuthClient.triggerAppConfigUpdate', async () => {
|
||||||
|
const appAuthClient = await createAppAuthClient();
|
||||||
|
|
||||||
|
const appAuthClientAfterUpdateSpy = vi.spyOn(
|
||||||
|
AppAuthClient.prototype,
|
||||||
|
'triggerAppConfigUpdate'
|
||||||
|
);
|
||||||
|
|
||||||
|
await appAuthClient.$query().patchAndFetch({ name: 'sample' });
|
||||||
|
|
||||||
|
expect(appAuthClientAfterUpdateSpy).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
it('$afterFind should call AppAuthClient.decryptData', async () => {
|
it('$afterFind should call AppAuthClient.decryptData', async () => {
|
||||||
const appAuthClient = await createAppAuthClient();
|
const appAuthClient = await createAppAuthClient();
|
||||||
|
|
||||||
|
@@ -16,7 +16,9 @@ class AppConfig extends Base {
|
|||||||
properties: {
|
properties: {
|
||||||
id: { type: 'string', format: 'uuid' },
|
id: { type: 'string', format: 'uuid' },
|
||||||
key: { type: 'string' },
|
key: { type: 'string' },
|
||||||
useOnlyPredefinedAuthClients: { type: 'boolean', default: false },
|
connectionAllowed: { type: 'boolean', default: false },
|
||||||
|
customConnectionAllowed: { type: 'boolean', default: false },
|
||||||
|
shared: { type: 'boolean', default: false },
|
||||||
disabled: { type: 'boolean', default: false },
|
disabled: { type: 'boolean', default: false },
|
||||||
createdAt: { type: 'string' },
|
createdAt: { type: 'string' },
|
||||||
updatedAt: { type: 'string' },
|
updatedAt: { type: 'string' },
|
||||||
@@ -39,6 +41,39 @@ class AppConfig extends Base {
|
|||||||
|
|
||||||
return await App.findOneByKey(this.key);
|
return await App.findOneByKey(this.key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async computeAndAssignConnectionAllowedProperty() {
|
||||||
|
this.connectionAllowed = await this.computeConnectionAllowedProperty();
|
||||||
|
}
|
||||||
|
|
||||||
|
async computeConnectionAllowedProperty() {
|
||||||
|
const appAuthClients = await this.$relatedQuery('appAuthClients');
|
||||||
|
|
||||||
|
const hasSomeActiveAppAuthClients =
|
||||||
|
appAuthClients?.some((appAuthClient) => appAuthClient.active) || false;
|
||||||
|
|
||||||
|
const conditions = [
|
||||||
|
hasSomeActiveAppAuthClients,
|
||||||
|
this.shared,
|
||||||
|
!this.disabled,
|
||||||
|
];
|
||||||
|
|
||||||
|
const connectionAllowed = conditions.every(Boolean);
|
||||||
|
|
||||||
|
return connectionAllowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
async $beforeInsert(queryContext) {
|
||||||
|
await super.$beforeInsert(queryContext);
|
||||||
|
|
||||||
|
await this.computeAndAssignConnectionAllowedProperty();
|
||||||
|
}
|
||||||
|
|
||||||
|
async $beforeUpdate(opt, queryContext) {
|
||||||
|
await super.$beforeUpdate(opt, queryContext);
|
||||||
|
|
||||||
|
await this.computeAndAssignConnectionAllowedProperty();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AppConfig;
|
export default AppConfig;
|
||||||
|
@@ -1,9 +1,11 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { vi, describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
import Base from './base.js';
|
import Base from './base.js';
|
||||||
import AppConfig from './app-config.js';
|
import AppConfig from './app-config.js';
|
||||||
import App from './app.js';
|
import App from './app.js';
|
||||||
import AppAuthClient from './app-auth-client.js';
|
import AppAuthClient from './app-auth-client.js';
|
||||||
|
import { createAppConfig } from '../../test/factories/app-config.js';
|
||||||
|
import { createAppAuthClient } from '../../test/factories/app-auth-client.js';
|
||||||
|
|
||||||
describe('AppConfig model', () => {
|
describe('AppConfig model', () => {
|
||||||
it('tableName should return correct name', () => {
|
it('tableName should return correct name', () => {
|
||||||
@@ -53,4 +55,126 @@ describe('AppConfig model', () => {
|
|||||||
expect(app).toStrictEqual(expectedApp);
|
expect(app).toStrictEqual(expectedApp);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('computeAndAssignConnectionAllowedProperty', () => {
|
||||||
|
it('should call computeConnectionAllowedProperty and assign the result', async () => {
|
||||||
|
const appConfig = await createAppConfig();
|
||||||
|
|
||||||
|
const computeConnectionAllowedPropertySpy = vi
|
||||||
|
.spyOn(appConfig, 'computeConnectionAllowedProperty')
|
||||||
|
.mockResolvedValue(true);
|
||||||
|
|
||||||
|
await appConfig.computeAndAssignConnectionAllowedProperty();
|
||||||
|
|
||||||
|
expect(computeConnectionAllowedPropertySpy).toHaveBeenCalled();
|
||||||
|
expect(appConfig.connectionAllowed).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('computeConnectionAllowedProperty', () => {
|
||||||
|
it('should return true when app is enabled, shared and allows custom connection with an active app auth client', async () => {
|
||||||
|
await createAppAuthClient({
|
||||||
|
appKey: 'deepl',
|
||||||
|
active: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await createAppAuthClient({
|
||||||
|
appKey: 'deepl',
|
||||||
|
active: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const appConfig = await createAppConfig({
|
||||||
|
disabled: false,
|
||||||
|
customConnectionAllowed: true,
|
||||||
|
shared: true,
|
||||||
|
key: 'deepl',
|
||||||
|
});
|
||||||
|
|
||||||
|
const connectionAllowed =
|
||||||
|
await appConfig.computeConnectionAllowedProperty();
|
||||||
|
|
||||||
|
expect(connectionAllowed).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false if there is no active app auth client', async () => {
|
||||||
|
await createAppAuthClient({
|
||||||
|
appKey: 'deepl',
|
||||||
|
active: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const appConfig = await createAppConfig({
|
||||||
|
disabled: false,
|
||||||
|
customConnectionAllowed: true,
|
||||||
|
shared: true,
|
||||||
|
key: 'deepl',
|
||||||
|
});
|
||||||
|
|
||||||
|
const connectionAllowed =
|
||||||
|
await appConfig.computeConnectionAllowedProperty();
|
||||||
|
|
||||||
|
expect(connectionAllowed).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false if there is no app auth clients', async () => {
|
||||||
|
const appConfig = await createAppConfig({
|
||||||
|
disabled: false,
|
||||||
|
customConnectionAllowed: true,
|
||||||
|
shared: true,
|
||||||
|
key: 'deepl',
|
||||||
|
});
|
||||||
|
|
||||||
|
const connectionAllowed =
|
||||||
|
await appConfig.computeConnectionAllowedProperty();
|
||||||
|
|
||||||
|
expect(connectionAllowed).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when app is disabled', async () => {
|
||||||
|
const appConfig = await createAppConfig({
|
||||||
|
disabled: true,
|
||||||
|
customConnectionAllowed: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const connectionAllowed =
|
||||||
|
await appConfig.computeConnectionAllowedProperty();
|
||||||
|
|
||||||
|
expect(connectionAllowed).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should return false when app doesn't allow custom connection`, async () => {
|
||||||
|
const appConfig = await createAppConfig({
|
||||||
|
disabled: false,
|
||||||
|
customConnectionAllowed: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const connectionAllowed =
|
||||||
|
await appConfig.computeConnectionAllowedProperty();
|
||||||
|
|
||||||
|
expect(connectionAllowed).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('$beforeInsert should call computeAndAssignConnectionAllowedProperty', async () => {
|
||||||
|
const computeAndAssignConnectionAllowedPropertySpy = vi
|
||||||
|
.spyOn(AppConfig.prototype, 'computeAndAssignConnectionAllowedProperty')
|
||||||
|
.mockResolvedValue(true);
|
||||||
|
|
||||||
|
await createAppConfig();
|
||||||
|
|
||||||
|
expect(computeAndAssignConnectionAllowedPropertySpy).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('$beforeUpdate should call computeAndAssignConnectionAllowedProperty', async () => {
|
||||||
|
const appConfig = await createAppConfig();
|
||||||
|
|
||||||
|
const computeAndAssignConnectionAllowedPropertySpy = vi
|
||||||
|
.spyOn(AppConfig.prototype, 'computeAndAssignConnectionAllowedProperty')
|
||||||
|
.mockResolvedValue(true);
|
||||||
|
|
||||||
|
await appConfig.$query().patch({
|
||||||
|
key: 'deepl',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(computeAndAssignConnectionAllowedPropertySpy).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -33,6 +33,10 @@ class Connection extends Base {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static get virtualAttributes() {
|
||||||
|
return ['reconnectable'];
|
||||||
|
}
|
||||||
|
|
||||||
static relationMappings = () => ({
|
static relationMappings = () => ({
|
||||||
user: {
|
user: {
|
||||||
relation: Base.BelongsToOneRelation,
|
relation: Base.BelongsToOneRelation,
|
||||||
@@ -79,6 +83,18 @@ class Connection extends Base {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
get reconnectable() {
|
||||||
|
if (this.appAuthClientId) {
|
||||||
|
return this.appAuthClient.active;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.appConfig) {
|
||||||
|
return !this.appConfig.disabled && this.appConfig.customConnectionAllowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
encryptData() {
|
encryptData() {
|
||||||
if (!this.eligibleForEncryption()) return;
|
if (!this.eligibleForEncryption()) return;
|
||||||
|
|
||||||
@@ -128,13 +144,19 @@ class Connection extends Base {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (appConfig.useOnlyPredefinedAuthClients && this.formattedData) {
|
if (!appConfig.customConnectionAllowed && this.formattedData) {
|
||||||
throw new NotAuthorizedError(
|
throw new NotAuthorizedError(
|
||||||
`New custom connections have been disabled for ${app.name}!`
|
`New custom connections have been disabled for ${app.name}!`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.formattedData) {
|
if (!appConfig.shared && this.appAuthClientId) {
|
||||||
|
throw new NotAuthorizedError(
|
||||||
|
'The connection with the given app auth client is not allowed!'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appConfig.shared && !this.formattedData) {
|
||||||
const authClient = await appConfig
|
const authClient = await appConfig
|
||||||
.$relatedQuery('appAuthClients')
|
.$relatedQuery('appAuthClients')
|
||||||
.findById(this.appAuthClientId)
|
.findById(this.appAuthClientId)
|
||||||
|
@@ -23,6 +23,14 @@ describe('Connection model', () => {
|
|||||||
expect(Connection.jsonSchema).toMatchSnapshot();
|
expect(Connection.jsonSchema).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('virtualAttributes should return correct attributes', () => {
|
||||||
|
const virtualAttributes = Connection.virtualAttributes;
|
||||||
|
|
||||||
|
const expectedAttributes = ['reconnectable'];
|
||||||
|
|
||||||
|
expect(virtualAttributes).toStrictEqual(expectedAttributes);
|
||||||
|
});
|
||||||
|
|
||||||
describe('relationMappings', () => {
|
describe('relationMappings', () => {
|
||||||
it('should return correct associations', () => {
|
it('should return correct associations', () => {
|
||||||
const relationMappings = Connection.relationMappings();
|
const relationMappings = Connection.relationMappings();
|
||||||
@@ -84,6 +92,78 @@ describe('Connection model', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('reconnectable', () => {
|
||||||
|
it('should return active status of app auth client when created via app auth client', async () => {
|
||||||
|
const appAuthClient = await createAppAuthClient({
|
||||||
|
active: true,
|
||||||
|
formattedAuthDefaults: {
|
||||||
|
clientId: 'sample-id',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const connection = await createConnection({
|
||||||
|
appAuthClientId: appAuthClient.id,
|
||||||
|
formattedData: {
|
||||||
|
token: 'sample-token',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const connectionWithAppAuthClient = await connection
|
||||||
|
.$query()
|
||||||
|
.withGraphFetched({
|
||||||
|
appAuthClient: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(connectionWithAppAuthClient.reconnectable).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when app config is not disabled and allows custom connection', async () => {
|
||||||
|
const appConfig = await createAppConfig({
|
||||||
|
key: 'gitlab',
|
||||||
|
disabled: false,
|
||||||
|
customConnectionAllowed: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const connection = await createConnection({
|
||||||
|
key: appConfig.key,
|
||||||
|
formattedData: {
|
||||||
|
token: 'sample-token',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const connectionWithAppAuthClient = await connection
|
||||||
|
.$query()
|
||||||
|
.withGraphFetched({
|
||||||
|
appConfig: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(connectionWithAppAuthClient.reconnectable).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when app config is disabled or does not allow custom connection', async () => {
|
||||||
|
const connection = await createConnection({
|
||||||
|
key: 'gitlab',
|
||||||
|
formattedData: {
|
||||||
|
token: 'sample-token',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await createAppConfig({
|
||||||
|
key: 'gitlab',
|
||||||
|
disabled: true,
|
||||||
|
customConnectionAllowed: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const connectionWithAppAuthClient = await connection
|
||||||
|
.$query()
|
||||||
|
.withGraphFetched({
|
||||||
|
appConfig: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(connectionWithAppAuthClient.reconnectable).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('encryptData', () => {
|
describe('encryptData', () => {
|
||||||
it('should return undefined if eligibleForEncryption is not true', async () => {
|
it('should return undefined if eligibleForEncryption is not true', async () => {
|
||||||
vi.spyOn(Connection.prototype, 'eligibleForEncryption').mockReturnValue(
|
vi.spyOn(Connection.prototype, 'eligibleForEncryption').mockReturnValue(
|
||||||
@@ -286,7 +366,6 @@ describe('Connection model', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: update test case name
|
|
||||||
it('should throw an error when app config does not allow custom connection with formatted data', async () => {
|
it('should throw an error when app config does not allow custom connection with formatted data', async () => {
|
||||||
vi.spyOn(Connection.prototype, 'getApp').mockResolvedValue({
|
vi.spyOn(Connection.prototype, 'getApp').mockResolvedValue({
|
||||||
name: 'gitlab',
|
name: 'gitlab',
|
||||||
@@ -294,7 +373,7 @@ describe('Connection model', () => {
|
|||||||
|
|
||||||
vi.spyOn(Connection.prototype, 'getAppConfig').mockResolvedValue({
|
vi.spyOn(Connection.prototype, 'getAppConfig').mockResolvedValue({
|
||||||
disabled: false,
|
disabled: false,
|
||||||
useOnlyPredefinedAuthClients: true,
|
customConnectionAllowed: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const connection = new Connection();
|
const connection = new Connection();
|
||||||
@@ -307,10 +386,32 @@ describe('Connection model', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should throw an error when app config is not shared with app auth client', async () => {
|
||||||
|
vi.spyOn(Connection.prototype, 'getApp').mockResolvedValue({
|
||||||
|
name: 'gitlab',
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.spyOn(Connection.prototype, 'getAppConfig').mockResolvedValue({
|
||||||
|
disabled: false,
|
||||||
|
shared: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const connection = new Connection();
|
||||||
|
connection.appAuthClientId = 'sample-id';
|
||||||
|
|
||||||
|
await expect(() =>
|
||||||
|
connection.checkEligibilityForCreation()
|
||||||
|
).rejects.toThrow(
|
||||||
|
'The connection with the given app auth client is not allowed!'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should apply app auth client auth defaults when creating with shared app auth client', async () => {
|
it('should apply app auth client auth defaults when creating with shared app auth client', async () => {
|
||||||
await createAppConfig({
|
await createAppConfig({
|
||||||
key: 'gitlab',
|
key: 'gitlab',
|
||||||
disabled: false,
|
disabled: false,
|
||||||
|
customConnectionAllowed: true,
|
||||||
|
shared: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const appAuthClient = await createAppAuthClient({
|
const appAuthClient = await createAppAuthClient({
|
||||||
|
@@ -88,13 +88,15 @@ class Flow extends Base {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
static async populateStatusProperty(flows) {
|
static async afterFind(args) {
|
||||||
const referenceFlow = flows[0];
|
const { result } = args;
|
||||||
|
|
||||||
|
const referenceFlow = result[0];
|
||||||
|
|
||||||
if (referenceFlow) {
|
if (referenceFlow) {
|
||||||
const shouldBePaused = await referenceFlow.isPaused();
|
const shouldBePaused = await referenceFlow.isPaused();
|
||||||
|
|
||||||
for (const flow of flows) {
|
for (const flow of result) {
|
||||||
if (!flow.active) {
|
if (!flow.active) {
|
||||||
flow.status = 'draft';
|
flow.status = 'draft';
|
||||||
} else if (flow.active && shouldBePaused) {
|
} else if (flow.active && shouldBePaused) {
|
||||||
@@ -106,10 +108,6 @@ class Flow extends Base {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async afterFind(args) {
|
|
||||||
await this.populateStatusProperty(args.result);
|
|
||||||
}
|
|
||||||
|
|
||||||
async lastInternalId() {
|
async lastInternalId() {
|
||||||
const lastExecution = await this.$relatedQuery('lastExecution');
|
const lastExecution = await this.$relatedQuery('lastExecution');
|
||||||
|
|
||||||
@@ -125,14 +123,13 @@ class Flow extends Base {
|
|||||||
return lastExecutions.map((execution) => execution.internalId);
|
return lastExecutions.map((execution) => execution.internalId);
|
||||||
}
|
}
|
||||||
|
|
||||||
static get IncompleteStepsError() {
|
get IncompleteStepsError() {
|
||||||
return new ValidationError({
|
return new ValidationError({
|
||||||
data: {
|
data: {
|
||||||
flow: [
|
flow: [
|
||||||
{
|
{
|
||||||
message:
|
message: 'All steps should be completed before updating flow status!'
|
||||||
'All steps should be completed before updating flow status!',
|
}
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
type: 'incompleteStepsError',
|
type: 'incompleteStepsError',
|
||||||
@@ -151,48 +148,36 @@ class Flow extends Base {
|
|||||||
type: 'action',
|
type: 'action',
|
||||||
position: 2,
|
position: 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return this.$query().withGraphFetched('steps');
|
||||||
}
|
}
|
||||||
|
|
||||||
async getStepById(stepId) {
|
async createActionStep(previousStepId) {
|
||||||
return await this.$relatedQuery('steps').findById(stepId).throwIfNotFound();
|
const previousStep = await this.$relatedQuery('steps')
|
||||||
}
|
.findById(previousStepId)
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
async insertActionStepAtPosition(position) {
|
const createdStep = await this.$relatedQuery('steps').insertAndFetch({
|
||||||
return await this.$relatedQuery('steps').insertAndFetch({
|
|
||||||
type: 'action',
|
type: 'action',
|
||||||
position,
|
position: previousStep.position + 1,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
async getStepsAfterPosition(position) {
|
const nextSteps = await this.$relatedQuery('steps')
|
||||||
return await this.$relatedQuery('steps').where('position', '>', position);
|
.where('position', '>=', createdStep.position)
|
||||||
}
|
.whereNot('id', createdStep.id);
|
||||||
|
|
||||||
async updateStepPositionsFrom(startPosition, steps) {
|
const nextStepQueries = nextSteps.map(async (nextStep, index) => {
|
||||||
const stepPositionUpdates = steps.map(async (step, index) => {
|
return await nextStep.$query().patchAndFetch({
|
||||||
return await step.$query().patch({
|
position: createdStep.position + index + 1,
|
||||||
position: startPosition + index,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return await Promise.all(stepPositionUpdates);
|
await Promise.all(nextStepQueries);
|
||||||
}
|
|
||||||
|
|
||||||
async createStepAfter(previousStepId) {
|
|
||||||
const previousStep = await this.getStepById(previousStepId);
|
|
||||||
|
|
||||||
const nextSteps = await this.getStepsAfterPosition(previousStep.position);
|
|
||||||
|
|
||||||
const createdStep = await this.insertActionStepAtPosition(
|
|
||||||
previousStep.position + 1
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.updateStepPositionsFrom(createdStep.position + 1, nextSteps);
|
|
||||||
|
|
||||||
return createdStep;
|
return createdStep;
|
||||||
}
|
}
|
||||||
|
|
||||||
async unregisterWebhook() {
|
async delete() {
|
||||||
const triggerStep = await this.getTriggerStep();
|
const triggerStep = await this.getTriggerStep();
|
||||||
const trigger = await triggerStep?.getTriggerCommand();
|
const trigger = await triggerStep?.getTriggerCommand();
|
||||||
|
|
||||||
@@ -213,33 +198,15 @@ class Flow extends Base {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
async deleteExecutionSteps() {
|
|
||||||
const executionIds = (
|
const executionIds = (
|
||||||
await this.$relatedQuery('executions').select('executions.id')
|
await this.$relatedQuery('executions').select('executions.id')
|
||||||
).map((execution) => execution.id);
|
).map((execution) => execution.id);
|
||||||
|
|
||||||
return await ExecutionStep.query()
|
await ExecutionStep.query().delete().whereIn('execution_id', executionIds);
|
||||||
.delete()
|
|
||||||
.whereIn('execution_id', executionIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteExecutions() {
|
|
||||||
return await this.$relatedQuery('executions').delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteSteps() {
|
|
||||||
return await this.$relatedQuery('steps').delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete() {
|
|
||||||
await this.unregisterWebhook();
|
|
||||||
|
|
||||||
await this.deleteExecutionSteps();
|
|
||||||
await this.deleteExecutions();
|
|
||||||
await this.deleteSteps();
|
|
||||||
|
|
||||||
|
await this.$relatedQuery('executions').delete();
|
||||||
|
await this.$relatedQuery('steps').delete();
|
||||||
await this.$query().delete();
|
await this.$query().delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,18 +291,6 @@ class Flow extends Base {
|
|||||||
return duplicatedFlowWithSteps;
|
return duplicatedFlowWithSteps;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTriggerStep() {
|
|
||||||
return await this.$relatedQuery('steps').findOne({
|
|
||||||
type: 'trigger',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async isPaused() {
|
|
||||||
const user = await this.$relatedQuery('user').withSoftDeleted();
|
|
||||||
const allowedToRunFlows = await user.isAllowedToRunFlows();
|
|
||||||
return allowedToRunFlows ? false : true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateStatus(newActiveValue) {
|
async updateStatus(newActiveValue) {
|
||||||
if (this.active === newActiveValue) {
|
if (this.active === newActiveValue) {
|
||||||
return this;
|
return this;
|
||||||
@@ -344,7 +299,7 @@ class Flow extends Base {
|
|||||||
const triggerStep = await this.getTriggerStep();
|
const triggerStep = await this.getTriggerStep();
|
||||||
|
|
||||||
if (triggerStep.status === 'incomplete') {
|
if (triggerStep.status === 'incomplete') {
|
||||||
throw Flow.IncompleteStepsError;
|
throw this.IncompleteStepsError;
|
||||||
}
|
}
|
||||||
|
|
||||||
const trigger = await triggerStep.getTriggerCommand();
|
const trigger = await triggerStep.getTriggerCommand();
|
||||||
@@ -398,55 +353,60 @@ class Flow extends Base {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async throwIfHavingIncompleteSteps() {
|
async $beforeUpdate(opt, queryContext) {
|
||||||
const incompleteStep = await this.$relatedQuery('steps').findOne({
|
await super.$beforeUpdate(opt, queryContext);
|
||||||
|
|
||||||
|
if (!this.active) return;
|
||||||
|
|
||||||
|
const oldFlow = opt.old;
|
||||||
|
|
||||||
|
const incompleteStep = await oldFlow.$relatedQuery('steps').findOne({
|
||||||
status: 'incomplete',
|
status: 'incomplete',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (incompleteStep) {
|
if (incompleteStep) {
|
||||||
throw Flow.IncompleteStepsError;
|
throw this.IncompleteStepsError;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
async throwIfHavingLessThanTwoSteps() {
|
const allSteps = await oldFlow.$relatedQuery('steps');
|
||||||
const allSteps = await this.$relatedQuery('steps');
|
|
||||||
|
|
||||||
if (allSteps.length < 2) {
|
if (allSteps.length < 2) {
|
||||||
throw new ValidationError({
|
throw new ValidationError({
|
||||||
data: {
|
data: {
|
||||||
flow: [
|
flow: [
|
||||||
{
|
{
|
||||||
message:
|
message: 'There should be at least one trigger and one action steps in the flow!'
|
||||||
'There should be at least one trigger and one action steps in the flow!',
|
}
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
type: 'insufficientStepsError',
|
type: 'insufficientStepsError',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
async $beforeUpdate(opt, queryContext) {
|
return;
|
||||||
await super.$beforeUpdate(opt, queryContext);
|
|
||||||
|
|
||||||
if (this.active) {
|
|
||||||
await opt.old.throwIfHavingIncompleteSteps();
|
|
||||||
|
|
||||||
await opt.old.throwIfHavingLessThanTwoSteps();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async $afterInsert(queryContext) {
|
async $afterInsert(queryContext) {
|
||||||
await super.$afterInsert(queryContext);
|
await super.$afterInsert(queryContext);
|
||||||
|
|
||||||
Telemetry.flowCreated(this);
|
Telemetry.flowCreated(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
async $afterUpdate(opt, queryContext) {
|
async $afterUpdate(opt, queryContext) {
|
||||||
await super.$afterUpdate(opt, queryContext);
|
await super.$afterUpdate(opt, queryContext);
|
||||||
|
|
||||||
Telemetry.flowUpdated(this);
|
Telemetry.flowUpdated(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getTriggerStep() {
|
||||||
|
return await this.$relatedQuery('steps').findOne({
|
||||||
|
type: 'trigger',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async isPaused() {
|
||||||
|
const user = await this.$relatedQuery('user').withSoftDeleted();
|
||||||
|
const allowedToRunFlows = await user.isAllowedToRunFlows();
|
||||||
|
return allowedToRunFlows ? false : true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Flow;
|
export default Flow;
|
||||||
|
@@ -1,616 +0,0 @@
|
|||||||
import { describe, it, expect, vi } from 'vitest';
|
|
||||||
import Flow from './flow.js';
|
|
||||||
import User from './user.js';
|
|
||||||
import Base from './base.js';
|
|
||||||
import Step from './step.js';
|
|
||||||
import Execution from './execution.js';
|
|
||||||
import Telemetry from '../helpers/telemetry/index.js';
|
|
||||||
import * as globalVariableModule from '../helpers/global-variable.js';
|
|
||||||
import { createFlow } from '../../test/factories/flow.js';
|
|
||||||
import { createStep } from '../../test/factories/step.js';
|
|
||||||
import { createExecution } from '../../test/factories/execution.js';
|
|
||||||
import { createExecutionStep } from '../../test/factories/execution-step.js';
|
|
||||||
|
|
||||||
describe('Flow model', () => {
|
|
||||||
it('tableName should return correct name', () => {
|
|
||||||
expect(Flow.tableName).toBe('flows');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('jsonSchema should have correct validations', () => {
|
|
||||||
expect(Flow.jsonSchema).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('relationMappings', () => {
|
|
||||||
it('should return correct associations', () => {
|
|
||||||
const relationMappings = Flow.relationMappings();
|
|
||||||
|
|
||||||
const expectedRelations = {
|
|
||||||
steps: {
|
|
||||||
relation: Base.HasManyRelation,
|
|
||||||
modelClass: Step,
|
|
||||||
join: {
|
|
||||||
from: 'flows.id',
|
|
||||||
to: 'steps.flow_id',
|
|
||||||
},
|
|
||||||
filter: expect.any(Function),
|
|
||||||
},
|
|
||||||
triggerStep: {
|
|
||||||
relation: Base.HasOneRelation,
|
|
||||||
modelClass: Step,
|
|
||||||
join: {
|
|
||||||
from: 'flows.id',
|
|
||||||
to: 'steps.flow_id',
|
|
||||||
},
|
|
||||||
filter: expect.any(Function),
|
|
||||||
},
|
|
||||||
executions: {
|
|
||||||
relation: Base.HasManyRelation,
|
|
||||||
modelClass: Execution,
|
|
||||||
join: {
|
|
||||||
from: 'flows.id',
|
|
||||||
to: 'executions.flow_id',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
lastExecution: {
|
|
||||||
relation: Base.HasOneRelation,
|
|
||||||
modelClass: Execution,
|
|
||||||
join: {
|
|
||||||
from: 'flows.id',
|
|
||||||
to: 'executions.flow_id',
|
|
||||||
},
|
|
||||||
filter: expect.any(Function),
|
|
||||||
},
|
|
||||||
user: {
|
|
||||||
relation: Base.HasOneRelation,
|
|
||||||
modelClass: User,
|
|
||||||
join: {
|
|
||||||
from: 'flows.user_id',
|
|
||||||
to: 'users.id',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(relationMappings).toStrictEqual(expectedRelations);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('steps should return the steps', () => {
|
|
||||||
const relations = Flow.relationMappings();
|
|
||||||
const orderBySpy = vi.fn();
|
|
||||||
|
|
||||||
relations.steps.filter({ orderBy: orderBySpy });
|
|
||||||
|
|
||||||
expect(orderBySpy).toHaveBeenCalledWith('position', 'asc');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('triggerStep should return the trigger step', () => {
|
|
||||||
const relations = Flow.relationMappings();
|
|
||||||
|
|
||||||
const firstSpy = vi.fn();
|
|
||||||
|
|
||||||
const limitSpy = vi.fn().mockImplementation(() => ({
|
|
||||||
first: firstSpy,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const whereSpy = vi.fn().mockImplementation(() => ({
|
|
||||||
limit: limitSpy,
|
|
||||||
}));
|
|
||||||
|
|
||||||
relations.triggerStep.filter({ where: whereSpy });
|
|
||||||
|
|
||||||
expect(whereSpy).toHaveBeenCalledWith('type', 'trigger');
|
|
||||||
expect(limitSpy).toHaveBeenCalledWith(1);
|
|
||||||
expect(firstSpy).toHaveBeenCalledOnce();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('lastExecution should return the last execution', () => {
|
|
||||||
const relations = Flow.relationMappings();
|
|
||||||
|
|
||||||
const firstSpy = vi.fn();
|
|
||||||
|
|
||||||
const limitSpy = vi.fn().mockImplementation(() => ({
|
|
||||||
first: firstSpy,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const orderBySpy = vi.fn().mockImplementation(() => ({
|
|
||||||
limit: limitSpy,
|
|
||||||
}));
|
|
||||||
|
|
||||||
relations.lastExecution.filter({ orderBy: orderBySpy });
|
|
||||||
|
|
||||||
expect(orderBySpy).toHaveBeenCalledWith('created_at', 'desc');
|
|
||||||
expect(limitSpy).toHaveBeenCalledWith(1);
|
|
||||||
expect(firstSpy).toHaveBeenCalledOnce();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('populateStatusProperty', () => {
|
|
||||||
it('should assign "draft" to status property when a flow is not active', async () => {
|
|
||||||
const referenceFlow = await createFlow({ active: false });
|
|
||||||
|
|
||||||
const flows = [referenceFlow];
|
|
||||||
|
|
||||||
vi.spyOn(referenceFlow, 'isPaused').mockResolvedValue();
|
|
||||||
|
|
||||||
await Flow.populateStatusProperty(flows);
|
|
||||||
|
|
||||||
expect(referenceFlow.status).toBe('draft');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should assign "paused" to status property when a flow is active, but should be paused', async () => {
|
|
||||||
const referenceFlow = await createFlow({ active: true });
|
|
||||||
|
|
||||||
const flows = [referenceFlow];
|
|
||||||
|
|
||||||
vi.spyOn(referenceFlow, 'isPaused').mockResolvedValue(true);
|
|
||||||
|
|
||||||
await Flow.populateStatusProperty(flows);
|
|
||||||
|
|
||||||
expect(referenceFlow.status).toBe('paused');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should assign "published" to status property when a flow is active', async () => {
|
|
||||||
const referenceFlow = await createFlow({ active: true });
|
|
||||||
|
|
||||||
const flows = [referenceFlow];
|
|
||||||
|
|
||||||
vi.spyOn(referenceFlow, 'isPaused').mockResolvedValue(false);
|
|
||||||
|
|
||||||
await Flow.populateStatusProperty(flows);
|
|
||||||
|
|
||||||
expect(referenceFlow.status).toBe('published');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('afterFind should call Flow.populateStatusProperty', async () => {
|
|
||||||
const populateStatusPropertySpy = vi
|
|
||||||
.spyOn(Flow, 'populateStatusProperty')
|
|
||||||
.mockImplementation(() => {});
|
|
||||||
|
|
||||||
await createFlow();
|
|
||||||
|
|
||||||
expect(populateStatusPropertySpy).toHaveBeenCalledOnce();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('lastInternalId', () => {
|
|
||||||
it('should return internal ID of last execution when exists', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
|
|
||||||
await createExecution({ flowId: flow.id });
|
|
||||||
await createExecution({ flowId: flow.id });
|
|
||||||
const lastExecution = await createExecution({ flowId: flow.id });
|
|
||||||
|
|
||||||
expect(await flow.lastInternalId()).toBe(lastExecution.internalId);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return null when no flow execution exists', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
|
|
||||||
expect(await flow.lastInternalId()).toBe(null);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('lastInternalIds', () => {
|
|
||||||
it('should return last internal IDs', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
|
|
||||||
const internalIds = [
|
|
||||||
await createExecution({ flowId: flow.id }),
|
|
||||||
await createExecution({ flowId: flow.id }),
|
|
||||||
await createExecution({ flowId: flow.id }),
|
|
||||||
].map((execution) => execution.internalId);
|
|
||||||
|
|
||||||
expect(await flow.lastInternalIds()).toStrictEqual(internalIds);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return last 50 internal IDs by default', async () => {
|
|
||||||
const flow = new Flow();
|
|
||||||
|
|
||||||
const limitSpy = vi.fn().mockResolvedValue([]);
|
|
||||||
|
|
||||||
vi.spyOn(flow, '$relatedQuery').mockReturnValue({
|
|
||||||
select: vi.fn().mockReturnThis(),
|
|
||||||
orderBy: vi.fn().mockReturnThis(),
|
|
||||||
limit: limitSpy,
|
|
||||||
});
|
|
||||||
|
|
||||||
await flow.lastInternalIds();
|
|
||||||
|
|
||||||
expect(limitSpy).toHaveBeenCalledWith(50);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('IncompleteStepsError should return validation error for incomplete steps', () => {
|
|
||||||
expect(() => {
|
|
||||||
throw Flow.IncompleteStepsError;
|
|
||||||
}).toThrowError(
|
|
||||||
'flow: All steps should be completed before updating flow status!'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('createInitialSteps should create one trigger and one action step', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
await flow.createInitialSteps();
|
|
||||||
const steps = await flow.$relatedQuery('steps');
|
|
||||||
|
|
||||||
expect(steps.length).toBe(2);
|
|
||||||
|
|
||||||
expect(steps[0]).toMatchObject({
|
|
||||||
flowId: flow.id,
|
|
||||||
type: 'trigger',
|
|
||||||
position: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(steps[1]).toMatchObject({
|
|
||||||
flowId: flow.id,
|
|
||||||
type: 'action',
|
|
||||||
position: 2,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getStepById should return the step with the given ID from the flow', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
|
|
||||||
const step = await createStep({ flowId: flow.id });
|
|
||||||
|
|
||||||
expect(await flow.getStepById(step.id)).toStrictEqual(step);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('insertActionStepAtPosition should insert action step at given position', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
|
|
||||||
await flow.createInitialSteps();
|
|
||||||
|
|
||||||
const createdStep = await flow.insertActionStepAtPosition(2);
|
|
||||||
|
|
||||||
expect(createdStep).toMatchObject({
|
|
||||||
type: 'action',
|
|
||||||
position: 2,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getStepsAfterPosition should return steps after the given position', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
|
|
||||||
await flow.createInitialSteps();
|
|
||||||
|
|
||||||
await createStep({ flowId: flow.id });
|
|
||||||
|
|
||||||
expect(await flow.getStepsAfterPosition(1)).toMatchObject([
|
|
||||||
{ position: 2 },
|
|
||||||
{ position: 3 },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('updateStepPositionsFrom', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
|
|
||||||
await createStep({ type: 'trigger', flowId: flow.id, position: 6 });
|
|
||||||
await createStep({ type: 'action', flowId: flow.id, position: 8 });
|
|
||||||
await createStep({ type: 'action', flowId: flow.id, position: 10 });
|
|
||||||
|
|
||||||
await flow.updateStepPositionsFrom(2, await flow.$relatedQuery('steps'));
|
|
||||||
|
|
||||||
expect(await flow.$relatedQuery('steps')).toMatchObject([
|
|
||||||
{ position: 2, type: 'trigger' },
|
|
||||||
{ position: 3, type: 'action' },
|
|
||||||
{ position: 4, type: 'action' },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('createStepAfter should create an action step after given step ID', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
|
|
||||||
const triggerStep = await createStep({ type: 'trigger', flowId: flow.id });
|
|
||||||
const actionStep = await createStep({ type: 'action', flowId: flow.id });
|
|
||||||
|
|
||||||
const createdStep = await flow.createStepAfter(triggerStep.id);
|
|
||||||
|
|
||||||
const refetchedActionStep = await actionStep.$query();
|
|
||||||
|
|
||||||
expect(createdStep).toMatchObject({ type: 'action', position: 2 });
|
|
||||||
expect(refetchedActionStep.position).toBe(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('unregisterWebhook', () => {
|
|
||||||
it('should unregister webhook on remote when supported', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
const triggerStep = await createStep({
|
|
||||||
flowId: flow.id,
|
|
||||||
appKey: 'typeform',
|
|
||||||
key: 'new-entry',
|
|
||||||
type: 'trigger',
|
|
||||||
});
|
|
||||||
|
|
||||||
const unregisterHookSpy = vi.fn().mockResolvedValue();
|
|
||||||
|
|
||||||
vi.spyOn(Step.prototype, 'getTriggerCommand').mockResolvedValue({
|
|
||||||
type: 'webhook',
|
|
||||||
unregisterHook: unregisterHookSpy,
|
|
||||||
});
|
|
||||||
|
|
||||||
const globalVariableSpy = vi
|
|
||||||
.spyOn(globalVariableModule, 'default')
|
|
||||||
.mockResolvedValue('global-variable');
|
|
||||||
|
|
||||||
await flow.unregisterWebhook();
|
|
||||||
|
|
||||||
expect(unregisterHookSpy).toHaveBeenCalledWith('global-variable');
|
|
||||||
expect(globalVariableSpy).toHaveBeenCalledWith({
|
|
||||||
flow,
|
|
||||||
step: triggerStep,
|
|
||||||
connection: undefined,
|
|
||||||
app: await triggerStep.getApp(),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should silently fail when unregistration fails', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
await createStep({
|
|
||||||
flowId: flow.id,
|
|
||||||
appKey: 'typeform',
|
|
||||||
key: 'new-entry',
|
|
||||||
type: 'trigger',
|
|
||||||
});
|
|
||||||
|
|
||||||
const unregisterHookSpy = vi.fn().mockRejectedValue(new Error());
|
|
||||||
|
|
||||||
vi.spyOn(Step.prototype, 'getTriggerCommand').mockResolvedValue({
|
|
||||||
type: 'webhook',
|
|
||||||
unregisterHook: unregisterHookSpy,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(await flow.unregisterWebhook()).toBe(undefined);
|
|
||||||
expect(unregisterHookSpy).toHaveBeenCalledOnce();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should do nothing when trigger step is not webhook', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
await createStep({
|
|
||||||
flowId: flow.id,
|
|
||||||
type: 'trigger',
|
|
||||||
});
|
|
||||||
|
|
||||||
const unregisterHookSpy = vi.fn().mockRejectedValue(new Error());
|
|
||||||
|
|
||||||
expect(await flow.unregisterWebhook()).toBe(undefined);
|
|
||||||
expect(unregisterHookSpy).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('deleteExecutionSteps should delete related execution steps', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
const execution = await createExecution({ flowId: flow.id });
|
|
||||||
const firstExecutionStep = await createExecutionStep({
|
|
||||||
executionId: execution.id,
|
|
||||||
});
|
|
||||||
const secondExecutionStep = await createExecutionStep({
|
|
||||||
executionId: execution.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
await flow.deleteExecutionSteps();
|
|
||||||
|
|
||||||
expect(await firstExecutionStep.$query()).toBe(undefined);
|
|
||||||
expect(await secondExecutionStep.$query()).toBe(undefined);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('deleteExecutions should delete related executions', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
const firstExecution = await createExecution({ flowId: flow.id });
|
|
||||||
const secondExecution = await createExecution({ flowId: flow.id });
|
|
||||||
|
|
||||||
await flow.deleteExecutions();
|
|
||||||
|
|
||||||
expect(await firstExecution.$query()).toBe(undefined);
|
|
||||||
expect(await secondExecution.$query()).toBe(undefined);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('deleteSteps should delete related steps', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
await flow.createInitialSteps();
|
|
||||||
await flow.deleteSteps();
|
|
||||||
|
|
||||||
expect(await flow.$relatedQuery('steps')).toStrictEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('delete should delete the flow with its relations', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
|
|
||||||
const unregisterWebhookSpy = vi
|
|
||||||
.spyOn(flow, 'unregisterWebhook')
|
|
||||||
.mockResolvedValue();
|
|
||||||
const deleteExecutionStepsSpy = vi
|
|
||||||
.spyOn(flow, 'deleteExecutionSteps')
|
|
||||||
.mockResolvedValue();
|
|
||||||
const deleteExecutionsSpy = vi
|
|
||||||
.spyOn(flow, 'deleteExecutions')
|
|
||||||
.mockResolvedValue();
|
|
||||||
const deleteStepsSpy = vi.spyOn(flow, 'deleteSteps').mockResolvedValue();
|
|
||||||
|
|
||||||
await flow.delete();
|
|
||||||
|
|
||||||
expect(unregisterWebhookSpy).toHaveBeenCalledOnce();
|
|
||||||
expect(deleteExecutionStepsSpy).toHaveBeenCalledOnce();
|
|
||||||
expect(deleteExecutionsSpy).toHaveBeenCalledOnce();
|
|
||||||
expect(deleteStepsSpy).toHaveBeenCalledOnce();
|
|
||||||
expect(await flow.$query()).toBe(undefined);
|
|
||||||
});
|
|
||||||
|
|
||||||
it.todo('duplicateFor');
|
|
||||||
|
|
||||||
it('getTriggerStep', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
const triggerStep = await createStep({ flowId: flow.id, type: 'trigger' });
|
|
||||||
|
|
||||||
await createStep({ flowId: flow.id, type: 'action' });
|
|
||||||
|
|
||||||
expect(await flow.getTriggerStep()).toStrictEqual(triggerStep);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('isPaused', () => {
|
|
||||||
it('should return true when user.isAllowedToRunFlows returns false', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
|
|
||||||
const isAllowedToRunFlowsSpy = vi.fn().mockResolvedValue(false);
|
|
||||||
vi.spyOn(flow, '$relatedQuery').mockReturnValue({
|
|
||||||
withSoftDeleted: vi.fn().mockReturnThis(),
|
|
||||||
isAllowedToRunFlows: isAllowedToRunFlowsSpy,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(await flow.isPaused()).toBe(true);
|
|
||||||
expect(isAllowedToRunFlowsSpy).toHaveBeenCalledOnce();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return false when user.isAllowedToRunFlows returns true', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
|
|
||||||
const isAllowedToRunFlowsSpy = vi.fn().mockResolvedValue(true);
|
|
||||||
vi.spyOn(flow, '$relatedQuery').mockReturnValue({
|
|
||||||
withSoftDeleted: vi.fn().mockReturnThis(),
|
|
||||||
isAllowedToRunFlows: isAllowedToRunFlowsSpy,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(await flow.isPaused()).toBe(false);
|
|
||||||
expect(isAllowedToRunFlowsSpy).toHaveBeenCalledOnce();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('throwIfHavingIncompleteSteps', () => {
|
|
||||||
it('should throw validation error with incomplete steps', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
|
|
||||||
await flow.createInitialSteps();
|
|
||||||
|
|
||||||
await expect(() =>
|
|
||||||
flow.throwIfHavingIncompleteSteps()
|
|
||||||
).rejects.toThrowError(
|
|
||||||
'flow: All steps should be completed before updating flow status!'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return undefined when all steps are completed', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
|
|
||||||
await createStep({
|
|
||||||
flowId: flow.id,
|
|
||||||
status: 'completed',
|
|
||||||
type: 'trigger',
|
|
||||||
});
|
|
||||||
|
|
||||||
await createStep({
|
|
||||||
flowId: flow.id,
|
|
||||||
status: 'completed',
|
|
||||||
type: 'action',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(await flow.throwIfHavingIncompleteSteps()).toBe(undefined);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('throwIfHavingLessThanTwoSteps', () => {
|
|
||||||
it('should throw validation error with less than two steps', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
|
|
||||||
await expect(() =>
|
|
||||||
flow.throwIfHavingLessThanTwoSteps()
|
|
||||||
).rejects.toThrowError(
|
|
||||||
'flow: There should be at least one trigger and one action steps in the flow!'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return undefined when there are at least two steps', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
|
|
||||||
await createStep({
|
|
||||||
flowId: flow.id,
|
|
||||||
type: 'trigger',
|
|
||||||
});
|
|
||||||
|
|
||||||
await createStep({
|
|
||||||
flowId: flow.id,
|
|
||||||
type: 'action',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(await flow.throwIfHavingLessThanTwoSteps()).toBe(undefined);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('$beforeUpdate', () => {
|
|
||||||
it('should invoke throwIfHavingIncompleteSteps when flow is becoming active', async () => {
|
|
||||||
const flow = await createFlow({ active: false });
|
|
||||||
|
|
||||||
const throwIfHavingIncompleteStepsSpy = vi
|
|
||||||
.spyOn(Flow.prototype, 'throwIfHavingIncompleteSteps')
|
|
||||||
.mockImplementation(() => {});
|
|
||||||
|
|
||||||
const throwIfHavingLessThanTwoStepsSpy = vi
|
|
||||||
.spyOn(Flow.prototype, 'throwIfHavingLessThanTwoSteps')
|
|
||||||
.mockImplementation(() => {});
|
|
||||||
|
|
||||||
await flow.$query().patch({ active: true });
|
|
||||||
|
|
||||||
expect(throwIfHavingIncompleteStepsSpy).toHaveBeenCalledOnce();
|
|
||||||
expect(throwIfHavingLessThanTwoStepsSpy).toHaveBeenCalledOnce();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should invoke throwIfHavingIncompleteSteps when flow is not becoming active', async () => {
|
|
||||||
const flow = await createFlow({ active: true });
|
|
||||||
|
|
||||||
const throwIfHavingIncompleteStepsSpy = vi
|
|
||||||
.spyOn(Flow.prototype, 'throwIfHavingIncompleteSteps')
|
|
||||||
.mockImplementation(() => {});
|
|
||||||
|
|
||||||
const throwIfHavingLessThanTwoStepsSpy = vi
|
|
||||||
.spyOn(Flow.prototype, 'throwIfHavingLessThanTwoSteps')
|
|
||||||
.mockImplementation(() => {});
|
|
||||||
|
|
||||||
await flow.$query().patch({});
|
|
||||||
|
|
||||||
expect(throwIfHavingIncompleteStepsSpy).not.toHaveBeenCalledOnce();
|
|
||||||
expect(throwIfHavingLessThanTwoStepsSpy).not.toHaveBeenCalledOnce();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('$afterInsert', () => {
|
|
||||||
it('should call super.$afterInsert', async () => {
|
|
||||||
const superAfterInsertSpy = vi.spyOn(Base.prototype, '$afterInsert');
|
|
||||||
|
|
||||||
await createFlow();
|
|
||||||
|
|
||||||
expect(superAfterInsertSpy).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call Telemetry.flowCreated', async () => {
|
|
||||||
const telemetryFlowCreatedSpy = vi
|
|
||||||
.spyOn(Telemetry, 'flowCreated')
|
|
||||||
.mockImplementation(() => {});
|
|
||||||
|
|
||||||
const flow = await createFlow();
|
|
||||||
|
|
||||||
expect(telemetryFlowCreatedSpy).toHaveBeenCalledWith(flow);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('$afterUpdate', () => {
|
|
||||||
it('should call super.$afterUpdate', async () => {
|
|
||||||
const superAfterUpdateSpy = vi.spyOn(Base.prototype, '$afterUpdate');
|
|
||||||
|
|
||||||
const flow = await createFlow();
|
|
||||||
|
|
||||||
await flow.$query().patch({ active: false });
|
|
||||||
|
|
||||||
expect(superAfterUpdateSpy).toHaveBeenCalledOnce();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('$afterUpdate should call Telemetry.flowUpdated', async () => {
|
|
||||||
const telemetryFlowUpdatedSpy = vi
|
|
||||||
.spyOn(Telemetry, 'flowUpdated')
|
|
||||||
.mockImplementation(() => {});
|
|
||||||
|
|
||||||
const flow = await createFlow();
|
|
||||||
|
|
||||||
await flow.$query().patch({ active: false });
|
|
||||||
|
|
||||||
expect(telemetryFlowUpdatedSpy).toHaveBeenCalled({});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@@ -5,7 +5,7 @@ import appConfig from '../config/app.js';
|
|||||||
import axios from '../helpers/axios-with-proxy.js';
|
import axios from '../helpers/axios-with-proxy.js';
|
||||||
import Base from './base.js';
|
import Base from './base.js';
|
||||||
import Identity from './identity.ee.js';
|
import Identity from './identity.ee.js';
|
||||||
import RoleMapping from './role-mapping.ee.js';
|
import SamlAuthProvidersRoleMapping from './saml-auth-providers-role-mapping.ee.js';
|
||||||
|
|
||||||
class SamlAuthProvider extends Base {
|
class SamlAuthProvider extends Base {
|
||||||
static tableName = 'saml_auth_providers';
|
static tableName = 'saml_auth_providers';
|
||||||
@@ -53,12 +53,12 @@ class SamlAuthProvider extends Base {
|
|||||||
to: 'saml_auth_providers.id',
|
to: 'saml_auth_providers.id',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
roleMappings: {
|
samlAuthProvidersRoleMappings: {
|
||||||
relation: Base.HasManyRelation,
|
relation: Base.HasManyRelation,
|
||||||
modelClass: RoleMapping,
|
modelClass: SamlAuthProvidersRoleMapping,
|
||||||
join: {
|
join: {
|
||||||
from: 'saml_auth_providers.id',
|
from: 'saml_auth_providers.id',
|
||||||
to: 'role_mappings.saml_auth_provider_id',
|
to: 'saml_auth_providers_role_mappings.saml_auth_provider_id',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -133,22 +133,27 @@ class SamlAuthProvider extends Base {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async updateRoleMappings(roleMappings) {
|
async updateRoleMappings(roleMappings) {
|
||||||
await this.$relatedQuery('roleMappings').delete();
|
return await SamlAuthProvider.transaction(async (trx) => {
|
||||||
|
await this.$relatedQuery('samlAuthProvidersRoleMappings', trx).delete();
|
||||||
|
|
||||||
if (isEmpty(roleMappings)) {
|
if (isEmpty(roleMappings)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const roleMappingsData = roleMappings.map((roleMapping) => ({
|
const samlAuthProvidersRoleMappingsData = roleMappings.map(
|
||||||
...roleMapping,
|
(samlAuthProvidersRoleMapping) => ({
|
||||||
samlAuthProviderId: this.id,
|
...samlAuthProvidersRoleMapping,
|
||||||
}));
|
samlAuthProviderId: this.id,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const newRoleMappings = await RoleMapping.query().insertAndFetch(
|
const samlAuthProvidersRoleMappings =
|
||||||
roleMappingsData
|
await SamlAuthProvidersRoleMapping.query(trx).insertAndFetch(
|
||||||
);
|
samlAuthProvidersRoleMappingsData
|
||||||
|
);
|
||||||
|
|
||||||
return newRoleMappings;
|
return samlAuthProvidersRoleMappings;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,231 +0,0 @@
|
|||||||
import { vi, beforeEach, describe, it, expect } from 'vitest';
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
import SamlAuthProvider from '../models/saml-auth-provider.ee';
|
|
||||||
import RoleMapping from '../models/role-mapping.ee';
|
|
||||||
import axios from '../helpers/axios-with-proxy.js';
|
|
||||||
import Identity from './identity.ee';
|
|
||||||
import Base from './base';
|
|
||||||
import appConfig from '../config/app';
|
|
||||||
import { createSamlAuthProvider } from '../../test/factories/saml-auth-provider.ee.js';
|
|
||||||
import { createRoleMapping } from '../../test/factories/role-mapping.js';
|
|
||||||
import { createRole } from '../../test/factories/role.js';
|
|
||||||
|
|
||||||
describe('SamlAuthProvider model', () => {
|
|
||||||
it('tableName should return correct name', () => {
|
|
||||||
expect(SamlAuthProvider.tableName).toBe('saml_auth_providers');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('jsonSchema should have the correct schema', () => {
|
|
||||||
expect(SamlAuthProvider.jsonSchema).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('relationMappings should return correct associations', () => {
|
|
||||||
const relationMappings = SamlAuthProvider.relationMappings();
|
|
||||||
|
|
||||||
const expectedRelations = {
|
|
||||||
identities: {
|
|
||||||
relation: Base.HasOneRelation,
|
|
||||||
modelClass: Identity,
|
|
||||||
join: {
|
|
||||||
from: 'identities.provider_id',
|
|
||||||
to: 'saml_auth_providers.id',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
roleMappings: {
|
|
||||||
relation: Base.HasManyRelation,
|
|
||||||
modelClass: RoleMapping,
|
|
||||||
join: {
|
|
||||||
from: 'saml_auth_providers.id',
|
|
||||||
to: 'role_mappings.saml_auth_provider_id',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(relationMappings).toStrictEqual(expectedRelations);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('virtualAttributes should return correct attributes', () => {
|
|
||||||
const virtualAttributes = SamlAuthProvider.virtualAttributes;
|
|
||||||
|
|
||||||
const expectedAttributes = ['loginUrl', 'remoteLogoutUrl'];
|
|
||||||
|
|
||||||
expect(virtualAttributes).toStrictEqual(expectedAttributes);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('loginUrl should return the URL of login', () => {
|
|
||||||
const samlAuthProvider = new SamlAuthProvider();
|
|
||||||
samlAuthProvider.issuer = 'sample-issuer';
|
|
||||||
|
|
||||||
vi.spyOn(appConfig, 'baseUrl', 'get').mockReturnValue(
|
|
||||||
'https://automatisch.io'
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(samlAuthProvider.loginUrl).toStrictEqual(
|
|
||||||
'https://automatisch.io/login/saml/sample-issuer'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('loginCallbackUrl should return the URL of login callback', () => {
|
|
||||||
const samlAuthProvider = new SamlAuthProvider();
|
|
||||||
samlAuthProvider.issuer = 'sample-issuer';
|
|
||||||
|
|
||||||
vi.spyOn(appConfig, 'baseUrl', 'get').mockReturnValue(
|
|
||||||
'https://automatisch.io'
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(samlAuthProvider.loginCallBackUrl).toStrictEqual(
|
|
||||||
'https://automatisch.io/login/saml/sample-issuer/callback'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('remoteLogoutUrl should return the URL from entrypoint', () => {
|
|
||||||
const samlAuthProvider = new SamlAuthProvider();
|
|
||||||
samlAuthProvider.entryPoint = 'https://example.com/saml/logout';
|
|
||||||
|
|
||||||
expect(samlAuthProvider.remoteLogoutUrl).toStrictEqual(
|
|
||||||
'https://example.com/saml/logout'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('config should return the correct configuration object', () => {
|
|
||||||
const samlAuthProvider = new SamlAuthProvider();
|
|
||||||
|
|
||||||
samlAuthProvider.certificate = 'sample-certificate';
|
|
||||||
samlAuthProvider.signatureAlgorithm = 'sha256';
|
|
||||||
samlAuthProvider.entryPoint = 'https://example.com/saml';
|
|
||||||
samlAuthProvider.issuer = 'sample-issuer';
|
|
||||||
|
|
||||||
vi.spyOn(appConfig, 'baseUrl', 'get').mockReturnValue(
|
|
||||||
'https://automatisch.io'
|
|
||||||
);
|
|
||||||
|
|
||||||
const expectedConfig = {
|
|
||||||
callbackUrl: 'https://automatisch.io/login/saml/sample-issuer/callback',
|
|
||||||
cert: 'sample-certificate',
|
|
||||||
entryPoint: 'https://example.com/saml',
|
|
||||||
issuer: 'sample-issuer',
|
|
||||||
signatureAlgorithm: 'sha256',
|
|
||||||
logoutUrl: 'https://example.com/saml',
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(samlAuthProvider.config).toStrictEqual(expectedConfig);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('generateLogoutRequestBody should return a correctly encoded SAML logout request', () => {
|
|
||||||
vi.mock('uuid', () => ({
|
|
||||||
v4: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const samlAuthProvider = new SamlAuthProvider();
|
|
||||||
|
|
||||||
samlAuthProvider.entryPoint = 'https://example.com/saml';
|
|
||||||
samlAuthProvider.issuer = 'sample-issuer';
|
|
||||||
|
|
||||||
const mockUuid = '123e4567-e89b-12d3-a456-426614174000';
|
|
||||||
uuidv4.mockReturnValue(mockUuid);
|
|
||||||
|
|
||||||
const sessionId = 'test-session-id';
|
|
||||||
|
|
||||||
const logoutRequest = samlAuthProvider.generateLogoutRequestBody(sessionId);
|
|
||||||
|
|
||||||
const expectedLogoutRequest = `
|
|
||||||
<samlp:LogoutRequest
|
|
||||||
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
|
|
||||||
ID="${mockUuid}"
|
|
||||||
Version="2.0"
|
|
||||||
IssueInstant="${new Date().toISOString()}"
|
|
||||||
Destination="https://example.com/saml">
|
|
||||||
|
|
||||||
<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">sample-issuer</saml:Issuer>
|
|
||||||
<samlp:SessionIndex>test-session-id</samlp:SessionIndex>
|
|
||||||
</samlp:LogoutRequest>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const expectedEncodedRequest = Buffer.from(expectedLogoutRequest).toString(
|
|
||||||
'base64'
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(logoutRequest).toBe(expectedEncodedRequest);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('terminateRemoteSession should send the correct POST request and return the response', async () => {
|
|
||||||
vi.mock('../helpers/axios-with-proxy.js', () => ({
|
|
||||||
default: {
|
|
||||||
post: vi.fn(),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const samlAuthProvider = new SamlAuthProvider();
|
|
||||||
|
|
||||||
samlAuthProvider.entryPoint = 'https://example.com/saml';
|
|
||||||
samlAuthProvider.generateLogoutRequestBody = vi
|
|
||||||
.fn()
|
|
||||||
.mockReturnValue('mockEncodedLogoutRequest');
|
|
||||||
|
|
||||||
const sessionId = 'test-session-id';
|
|
||||||
|
|
||||||
const mockResponse = { data: 'Logout Successful' };
|
|
||||||
axios.post.mockResolvedValue(mockResponse);
|
|
||||||
|
|
||||||
const response = await samlAuthProvider.terminateRemoteSession(sessionId);
|
|
||||||
|
|
||||||
expect(samlAuthProvider.generateLogoutRequestBody).toHaveBeenCalledWith(
|
|
||||||
sessionId
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(axios.post).toHaveBeenCalledWith(
|
|
||||||
'https://example.com/saml',
|
|
||||||
'SAMLRequest=mockEncodedLogoutRequest',
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(response).toBe(mockResponse);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('updateRoleMappings', () => {
|
|
||||||
let samlAuthProvider;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
samlAuthProvider = await createSamlAuthProvider();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should remove all existing role mappings', async () => {
|
|
||||||
await createRoleMapping({
|
|
||||||
samlAuthProviderId: samlAuthProvider.id,
|
|
||||||
remoteRoleName: 'Admin',
|
|
||||||
});
|
|
||||||
|
|
||||||
await createRoleMapping({
|
|
||||||
samlAuthProviderId: samlAuthProvider.id,
|
|
||||||
remoteRoleName: 'User',
|
|
||||||
});
|
|
||||||
|
|
||||||
await samlAuthProvider.updateRoleMappings([]);
|
|
||||||
|
|
||||||
const roleMappings = await samlAuthProvider.$relatedQuery('roleMappings');
|
|
||||||
expect(roleMappings).toStrictEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return the updated role mappings when new ones are provided', async () => {
|
|
||||||
const adminRole = await createRole({ name: 'Admin' });
|
|
||||||
const userRole = await createRole({ name: 'User' });
|
|
||||||
|
|
||||||
const newRoleMappings = [
|
|
||||||
{ remoteRoleName: 'Admin', roleId: adminRole.id },
|
|
||||||
{ remoteRoleName: 'User', roleId: userRole.id },
|
|
||||||
];
|
|
||||||
|
|
||||||
const result = await samlAuthProvider.updateRoleMappings(newRoleMappings);
|
|
||||||
|
|
||||||
const refetchedRoleMappings = await samlAuthProvider.$relatedQuery(
|
|
||||||
'roleMappings'
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toStrictEqual(refetchedRoleMappings);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@@ -1,8 +1,8 @@
|
|||||||
import Base from './base.js';
|
import Base from './base.js';
|
||||||
import SamlAuthProvider from './saml-auth-provider.ee.js';
|
import SamlAuthProvider from './saml-auth-provider.ee.js';
|
||||||
|
|
||||||
class RoleMapping extends Base {
|
class SamlAuthProvidersRoleMapping extends Base {
|
||||||
static tableName = 'role_mappings';
|
static tableName = 'saml_auth_providers_role_mappings';
|
||||||
|
|
||||||
static jsonSchema = {
|
static jsonSchema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
@@ -21,11 +21,11 @@ class RoleMapping extends Base {
|
|||||||
relation: Base.BelongsToOneRelation,
|
relation: Base.BelongsToOneRelation,
|
||||||
modelClass: SamlAuthProvider,
|
modelClass: SamlAuthProvider,
|
||||||
join: {
|
join: {
|
||||||
from: 'role_mappings.saml_auth_provider_id',
|
from: 'saml_auth_providers_role_mappings.saml_auth_provider_id',
|
||||||
to: 'saml_auth_providers.id',
|
to: 'saml_auth_providers.id',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default RoleMapping;
|
export default SamlAuthProvidersRoleMapping;
|
@@ -1,26 +1,28 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import RoleMapping from './role-mapping.ee';
|
import SamlAuthProvidersRoleMapping from '../models/saml-auth-providers-role-mapping.ee';
|
||||||
import SamlAuthProvider from './saml-auth-provider.ee';
|
import SamlAuthProvider from './saml-auth-provider.ee';
|
||||||
import Base from './base';
|
import Base from './base';
|
||||||
|
|
||||||
describe('RoleMapping model', () => {
|
describe('SamlAuthProvidersRoleMapping model', () => {
|
||||||
it('tableName should return correct name', () => {
|
it('tableName should return correct name', () => {
|
||||||
expect(RoleMapping.tableName).toBe('role_mappings');
|
expect(SamlAuthProvidersRoleMapping.tableName).toBe(
|
||||||
|
'saml_auth_providers_role_mappings'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('jsonSchema should have the correct schema', () => {
|
it('jsonSchema should have the correct schema', () => {
|
||||||
expect(RoleMapping.jsonSchema).toMatchSnapshot();
|
expect(SamlAuthProvidersRoleMapping.jsonSchema).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('relationMappings should return correct associations', () => {
|
it('relationMappings should return correct associations', () => {
|
||||||
const relationMappings = RoleMapping.relationMappings();
|
const relationMappings = SamlAuthProvidersRoleMapping.relationMappings();
|
||||||
|
|
||||||
const expectedRelations = {
|
const expectedRelations = {
|
||||||
samlAuthProvider: {
|
samlAuthProvider: {
|
||||||
relation: Base.BelongsToOneRelation,
|
relation: Base.BelongsToOneRelation,
|
||||||
modelClass: SamlAuthProvider,
|
modelClass: SamlAuthProvider,
|
||||||
join: {
|
join: {
|
||||||
from: 'role_mappings.saml_auth_provider_id',
|
from: 'saml_auth_providers_role_mappings.saml_auth_provider_id',
|
||||||
to: 'saml_auth_providers.id',
|
to: 'saml_auth_providers.id',
|
||||||
},
|
},
|
||||||
},
|
},
|
@@ -93,14 +93,6 @@ class Step extends Base {
|
|||||||
return `${appConfig.baseUrl}/apps/${this.appKey}/assets/favicon.svg`;
|
return `${appConfig.baseUrl}/apps/${this.appKey}/assets/favicon.svg`;
|
||||||
}
|
}
|
||||||
|
|
||||||
get isTrigger() {
|
|
||||||
return this.type === 'trigger';
|
|
||||||
}
|
|
||||||
|
|
||||||
get isAction() {
|
|
||||||
return this.type === 'action';
|
|
||||||
}
|
|
||||||
|
|
||||||
async computeWebhookPath() {
|
async computeWebhookPath() {
|
||||||
if (this.type === 'action') return null;
|
if (this.type === 'action') return null;
|
||||||
|
|
||||||
@@ -143,6 +135,24 @@ class Step extends Base {
|
|||||||
return webhookUrl;
|
return webhookUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async $afterInsert(queryContext) {
|
||||||
|
await super.$afterInsert(queryContext);
|
||||||
|
Telemetry.stepCreated(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
async $afterUpdate(opt, queryContext) {
|
||||||
|
await super.$afterUpdate(opt, queryContext);
|
||||||
|
Telemetry.stepUpdated(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
get isTrigger() {
|
||||||
|
return this.type === 'trigger';
|
||||||
|
}
|
||||||
|
|
||||||
|
get isAction() {
|
||||||
|
return this.type === 'action';
|
||||||
|
}
|
||||||
|
|
||||||
async getApp() {
|
async getApp() {
|
||||||
if (!this.appKey) return null;
|
if (!this.appKey) return null;
|
||||||
|
|
||||||
@@ -160,7 +170,12 @@ class Step extends Base {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getLastExecutionStep() {
|
async getLastExecutionStep() {
|
||||||
return await this.$relatedQuery('lastExecutionStep');
|
const lastExecutionStep = await this.$relatedQuery('executionSteps')
|
||||||
|
.orderBy('created_at', 'desc')
|
||||||
|
.limit(1)
|
||||||
|
.first();
|
||||||
|
|
||||||
|
return lastExecutionStep;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getNextStep() {
|
async getNextStep() {
|
||||||
@@ -192,18 +207,19 @@ class Step extends Base {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getSetupFields() {
|
async getSetupFields() {
|
||||||
let substeps;
|
let setupSupsteps;
|
||||||
|
|
||||||
if (this.isTrigger) {
|
if (this.isTrigger) {
|
||||||
substeps = (await this.getTriggerCommand()).substeps;
|
setupSupsteps = (await this.getTriggerCommand()).substeps;
|
||||||
} else {
|
} else {
|
||||||
substeps = (await this.getActionCommand()).substeps;
|
setupSupsteps = (await this.getActionCommand()).substeps;
|
||||||
}
|
}
|
||||||
|
|
||||||
const setupSubstep = substeps.find(
|
const existingArguments = setupSupsteps.find(
|
||||||
(substep) => substep.key === 'chooseTrigger'
|
(substep) => substep.key === 'chooseTrigger'
|
||||||
);
|
).arguments;
|
||||||
return setupSubstep.arguments;
|
|
||||||
|
return existingArguments;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSetupAndDynamicFields() {
|
async getSetupAndDynamicFields() {
|
||||||
@@ -310,17 +326,23 @@ class Step extends Base {
|
|||||||
.$relatedQuery('steps')
|
.$relatedQuery('steps')
|
||||||
.where('position', '>', this.position);
|
.where('position', '>', this.position);
|
||||||
|
|
||||||
await flow.updateStepPositionsFrom(this.position, nextSteps);
|
const nextStepQueries = nextSteps.map(async (nextStep) => {
|
||||||
|
await nextStep.$query().patch({
|
||||||
|
position: nextStep.position - 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(nextStepQueries);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateFor(user, newStepData) {
|
async updateFor(user, newStepData) {
|
||||||
const { appKey = this.appKey, connectionId, key, parameters } = newStepData;
|
const { connectionId, appKey, key, parameters } = newStepData;
|
||||||
|
|
||||||
if (connectionId && appKey) {
|
if (connectionId && (appKey || this.appKey)) {
|
||||||
await user.authorizedConnections
|
await user.authorizedConnections
|
||||||
.findOne({
|
.findOne({
|
||||||
id: connectionId,
|
id: connectionId,
|
||||||
key: appKey,
|
key: appKey || this.appKey,
|
||||||
})
|
})
|
||||||
.throwIfNotFound();
|
.throwIfNotFound();
|
||||||
}
|
}
|
||||||
@@ -334,8 +356,8 @@ class Step extends Base {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const updatedStep = await this.$query().patchAndFetch({
|
const updatedStep = await this.$query().patchAndFetch({
|
||||||
key,
|
key: key,
|
||||||
appKey,
|
appKey: appKey,
|
||||||
connectionId: connectionId,
|
connectionId: connectionId,
|
||||||
parameters: parameters,
|
parameters: parameters,
|
||||||
status: 'incomplete',
|
status: 'incomplete',
|
||||||
@@ -345,16 +367,6 @@ class Step extends Base {
|
|||||||
|
|
||||||
return updatedStep;
|
return updatedStep;
|
||||||
}
|
}
|
||||||
|
|
||||||
async $afterInsert(queryContext) {
|
|
||||||
await super.$afterInsert(queryContext);
|
|
||||||
Telemetry.stepCreated(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
async $afterUpdate(opt, queryContext) {
|
|
||||||
await super.$afterUpdate(opt, queryContext);
|
|
||||||
Telemetry.stepUpdated(this);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Step;
|
export default Step;
|
||||||
|
@@ -1,504 +0,0 @@
|
|||||||
import { beforeEach, describe, it, expect, vi } from 'vitest';
|
|
||||||
import appConfig from '../config/app.js';
|
|
||||||
import App from './app.js';
|
|
||||||
import Base from './base.js';
|
|
||||||
import Step from './step.js';
|
|
||||||
import Flow from './flow.js';
|
|
||||||
import Connection from './connection.js';
|
|
||||||
import ExecutionStep from './execution-step.js';
|
|
||||||
import Telemetry from '../helpers/telemetry/index.js';
|
|
||||||
import * as testRunModule from '../services/test-run.js';
|
|
||||||
import { createFlow } from '../../test/factories/flow.js';
|
|
||||||
import { createUser } from '../../test/factories/user.js';
|
|
||||||
import { createRole } from '../../test/factories/role.js';
|
|
||||||
import { createPermission } from '../../test/factories/permission.js';
|
|
||||||
import { createConnection } from '../../test/factories/connection.js';
|
|
||||||
import { createStep } from '../../test/factories/step.js';
|
|
||||||
import { createExecutionStep } from '../../test/factories/execution-step.js';
|
|
||||||
|
|
||||||
describe('Step model', () => {
|
|
||||||
it('tableName should return correct name', () => {
|
|
||||||
expect(Step.tableName).toBe('steps');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('jsonSchema should have correct validations', () => {
|
|
||||||
expect(Step.jsonSchema).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('virtualAttributes should return correct attributes', () => {
|
|
||||||
const virtualAttributes = Step.virtualAttributes;
|
|
||||||
|
|
||||||
const expectedAttributes = ['iconUrl', 'webhookUrl'];
|
|
||||||
|
|
||||||
expect(virtualAttributes).toStrictEqual(expectedAttributes);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('relationMappings', () => {
|
|
||||||
it('should return correct associations', () => {
|
|
||||||
const relationMappings = Step.relationMappings();
|
|
||||||
|
|
||||||
const expectedRelations = {
|
|
||||||
flow: {
|
|
||||||
relation: Base.BelongsToOneRelation,
|
|
||||||
modelClass: Flow,
|
|
||||||
join: {
|
|
||||||
from: 'steps.flow_id',
|
|
||||||
to: 'flows.id',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
connection: {
|
|
||||||
relation: Base.HasOneRelation,
|
|
||||||
modelClass: Connection,
|
|
||||||
join: {
|
|
||||||
from: 'steps.connection_id',
|
|
||||||
to: 'connections.id',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
lastExecutionStep: {
|
|
||||||
relation: Base.HasOneRelation,
|
|
||||||
modelClass: ExecutionStep,
|
|
||||||
join: {
|
|
||||||
from: 'steps.id',
|
|
||||||
to: 'execution_steps.step_id',
|
|
||||||
},
|
|
||||||
filter: expect.any(Function),
|
|
||||||
},
|
|
||||||
executionSteps: {
|
|
||||||
relation: Base.HasManyRelation,
|
|
||||||
modelClass: ExecutionStep,
|
|
||||||
join: {
|
|
||||||
from: 'steps.id',
|
|
||||||
to: 'execution_steps.step_id',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(relationMappings).toStrictEqual(expectedRelations);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('lastExecutionStep should return the trigger step', () => {
|
|
||||||
const relations = Step.relationMappings();
|
|
||||||
|
|
||||||
const firstSpy = vi.fn();
|
|
||||||
|
|
||||||
const limitSpy = vi.fn().mockImplementation(() => ({
|
|
||||||
first: firstSpy,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const orderBySpy = vi.fn().mockImplementation(() => ({
|
|
||||||
limit: limitSpy,
|
|
||||||
}));
|
|
||||||
|
|
||||||
relations.lastExecutionStep.filter({ orderBy: orderBySpy });
|
|
||||||
|
|
||||||
expect(orderBySpy).toHaveBeenCalledWith('created_at', 'desc');
|
|
||||||
expect(limitSpy).toHaveBeenCalledWith(1);
|
|
||||||
expect(firstSpy).toHaveBeenCalledOnce();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('webhookUrl', () => {
|
|
||||||
it('should return it along with appConfig.webhookUrl when exists', () => {
|
|
||||||
vi.spyOn(appConfig, 'webhookUrl', 'get').mockReturnValue(
|
|
||||||
'https://automatisch.io'
|
|
||||||
);
|
|
||||||
|
|
||||||
const step = new Step();
|
|
||||||
step.webhookPath = '/webhook-path';
|
|
||||||
|
|
||||||
expect(step.webhookUrl).toBe('https://automatisch.io/webhook-path');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return null when webhookUrl does not exist', () => {
|
|
||||||
const step = new Step();
|
|
||||||
|
|
||||||
expect(step.webhookUrl).toBe(null);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('iconUrl', () => {
|
|
||||||
it('should return step app icon absolute URL when app is set', () => {
|
|
||||||
vi.spyOn(appConfig, 'baseUrl', 'get').mockReturnValue(
|
|
||||||
'https://automatisch.io'
|
|
||||||
);
|
|
||||||
|
|
||||||
const step = new Step();
|
|
||||||
step.appKey = 'gitlab';
|
|
||||||
|
|
||||||
expect(step.iconUrl).toBe(
|
|
||||||
'https://automatisch.io/apps/gitlab/assets/favicon.svg'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return null when appKey is not set', () => {
|
|
||||||
const step = new Step();
|
|
||||||
|
|
||||||
expect(step.iconUrl).toBe(null);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('isTrigger should return true when step type is trigger', () => {
|
|
||||||
const step = new Step();
|
|
||||||
step.type = 'trigger';
|
|
||||||
|
|
||||||
expect(step.isTrigger).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('isAction should return true when step type is action', () => {
|
|
||||||
const step = new Step();
|
|
||||||
step.type = 'action';
|
|
||||||
|
|
||||||
expect(step.isAction).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe.todo('computeWebhookPath');
|
|
||||||
|
|
||||||
describe('getWebhookUrl', () => {
|
|
||||||
it('should return absolute webhook URL when step type is trigger', async () => {
|
|
||||||
const step = new Step();
|
|
||||||
step.type = 'trigger';
|
|
||||||
|
|
||||||
vi.spyOn(step, 'computeWebhookPath').mockResolvedValue('/webhook-path');
|
|
||||||
vi.spyOn(appConfig, 'webhookUrl', 'get').mockReturnValue(
|
|
||||||
'https://automatisch.io'
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(await step.getWebhookUrl()).toBe(
|
|
||||||
'https://automatisch.io/webhook-path'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return undefined when step type is action', async () => {
|
|
||||||
const step = new Step();
|
|
||||||
step.type = 'action';
|
|
||||||
|
|
||||||
expect(await step.getWebhookUrl()).toBe(undefined);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('getApp', () => {
|
|
||||||
it('should return app with the given appKey', async () => {
|
|
||||||
const step = new Step();
|
|
||||||
step.appKey = 'gitlab';
|
|
||||||
|
|
||||||
const findOneByKeySpy = vi.spyOn(App, 'findOneByKey').mockResolvedValue();
|
|
||||||
|
|
||||||
await step.getApp();
|
|
||||||
expect(findOneByKeySpy).toHaveBeenCalledWith('gitlab');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return null with no appKey', async () => {
|
|
||||||
const step = new Step();
|
|
||||||
|
|
||||||
const findOneByKeySpy = vi.spyOn(App, 'findOneByKey').mockResolvedValue();
|
|
||||||
|
|
||||||
expect(await step.getApp()).toBe(null);
|
|
||||||
expect(findOneByKeySpy).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('test should execute the flow and mark the step as completed', async () => {
|
|
||||||
const step = await createStep({ status: 'incomplete' });
|
|
||||||
|
|
||||||
const testRunSpy = vi.spyOn(testRunModule, 'default').mockResolvedValue();
|
|
||||||
|
|
||||||
const updatedStep = await step.test();
|
|
||||||
|
|
||||||
expect(testRunSpy).toHaveBeenCalledWith({ stepId: step.id });
|
|
||||||
expect(updatedStep.status).toBe('completed');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getLastExecutionStep should return last execution step', async () => {
|
|
||||||
const step = await createStep();
|
|
||||||
await createExecutionStep({ stepId: step.id });
|
|
||||||
const secondExecutionStep = await createExecutionStep({ stepId: step.id });
|
|
||||||
|
|
||||||
expect(await step.getLastExecutionStep()).toStrictEqual(
|
|
||||||
secondExecutionStep
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getNextStep should return the next step', async () => {
|
|
||||||
const firstStep = await createStep();
|
|
||||||
const secondStep = await createStep({ flowId: firstStep.flowId });
|
|
||||||
const thirdStep = await createStep({ flowId: firstStep.flowId });
|
|
||||||
|
|
||||||
expect(await secondStep.getNextStep()).toStrictEqual(thirdStep);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getTriggerCommand', () => {
|
|
||||||
it('should return trigger command when app key and key are defined in trigger step', async () => {
|
|
||||||
const step = new Step();
|
|
||||||
step.type = 'trigger';
|
|
||||||
step.appKey = 'webhook';
|
|
||||||
step.key = 'catchRawWebhook';
|
|
||||||
|
|
||||||
const findOneByKeySpy = vi.spyOn(App, 'findOneByKey');
|
|
||||||
const triggerCommand = await step.getTriggerCommand();
|
|
||||||
|
|
||||||
expect(findOneByKeySpy).toHaveBeenCalledWith(step.appKey);
|
|
||||||
expect(triggerCommand.key).toBe(step.key);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return null when key is not defined', async () => {
|
|
||||||
const step = new Step();
|
|
||||||
step.type = 'trigger';
|
|
||||||
step.appKey = 'webhook';
|
|
||||||
|
|
||||||
expect(await step.getTriggerCommand()).toBe(null);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getActionCommand', () => {
|
|
||||||
it('should return action comamand when app key and key are defined in action step', async () => {
|
|
||||||
const step = new Step();
|
|
||||||
step.type = 'action';
|
|
||||||
step.appKey = 'ntfy';
|
|
||||||
step.key = 'sendMessage';
|
|
||||||
|
|
||||||
const findOneByKeySpy = vi.spyOn(App, 'findOneByKey');
|
|
||||||
const actionCommand = await step.getActionCommand();
|
|
||||||
|
|
||||||
expect(findOneByKeySpy).toHaveBeenCalledWith(step.appKey);
|
|
||||||
expect(actionCommand.key).toBe(step.key);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return null when key is not defined', async () => {
|
|
||||||
const step = new Step();
|
|
||||||
step.type = 'action';
|
|
||||||
step.appKey = 'ntfy';
|
|
||||||
|
|
||||||
expect(await step.getActionCommand()).toBe(null);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getSetupFields', () => {
|
|
||||||
it('should return trigger setup substep fields in trigger step', async () => {
|
|
||||||
const step = new Step();
|
|
||||||
step.appKey = 'webhook';
|
|
||||||
step.key = 'catchRawWebhook';
|
|
||||||
step.type = 'trigger';
|
|
||||||
|
|
||||||
expect(await step.getSetupFields()).toStrictEqual([
|
|
||||||
{
|
|
||||||
label: 'Wait until flow is done',
|
|
||||||
key: 'workSynchronously',
|
|
||||||
type: 'dropdown',
|
|
||||||
required: true,
|
|
||||||
options: [
|
|
||||||
{ label: 'Yes', value: true },
|
|
||||||
{ label: 'No', value: false },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return action setup substep fields in action step', async () => {
|
|
||||||
const step = new Step();
|
|
||||||
step.appKey = 'datastore';
|
|
||||||
step.key = 'getValue';
|
|
||||||
step.type = 'action';
|
|
||||||
|
|
||||||
expect(await step.getSetupFields()).toStrictEqual([
|
|
||||||
{
|
|
||||||
label: 'Key',
|
|
||||||
key: 'key',
|
|
||||||
type: 'string',
|
|
||||||
required: true,
|
|
||||||
description: 'The key of your value to get.',
|
|
||||||
variables: true,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it.todo('getSetupAndDynamicFields');
|
|
||||||
it.todo('createDynamicFields');
|
|
||||||
it.todo('createDynamicData');
|
|
||||||
it.todo('updateWebhookUrl');
|
|
||||||
|
|
||||||
describe('delete', () => {
|
|
||||||
it('should delete the step and align the positions', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
await createStep({ flowId: flow.id, position: 1, type: 'trigger' });
|
|
||||||
await createStep({ flowId: flow.id, position: 2 });
|
|
||||||
const stepToDelete = await createStep({ flowId: flow.id, position: 3 });
|
|
||||||
await createStep({ flowId: flow.id, position: 4 });
|
|
||||||
|
|
||||||
await stepToDelete.delete();
|
|
||||||
|
|
||||||
const steps = await flow.$relatedQuery('steps');
|
|
||||||
const stepIds = steps.map((step) => step.id);
|
|
||||||
|
|
||||||
expect(stepIds).not.toContain(stepToDelete.id);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should align the positions of remaining steps', async () => {
|
|
||||||
const flow = await createFlow();
|
|
||||||
await createStep({ flowId: flow.id, position: 1, type: 'trigger' });
|
|
||||||
await createStep({ flowId: flow.id, position: 2 });
|
|
||||||
const stepToDelete = await createStep({ flowId: flow.id, position: 3 });
|
|
||||||
await createStep({ flowId: flow.id, position: 4 });
|
|
||||||
|
|
||||||
await stepToDelete.delete();
|
|
||||||
|
|
||||||
const steps = await flow.$relatedQuery('steps');
|
|
||||||
const stepPositions = steps.map((step) => step.position);
|
|
||||||
|
|
||||||
expect(stepPositions).toMatchObject([1, 2, 3]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should delete related execution steps', async () => {
|
|
||||||
const step = await createStep();
|
|
||||||
const executionStep = await createExecutionStep({ stepId: step.id });
|
|
||||||
|
|
||||||
await step.delete();
|
|
||||||
|
|
||||||
expect(await executionStep.$query()).toBe(undefined);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('updateFor', async () => {
|
|
||||||
let step,
|
|
||||||
userRole,
|
|
||||||
user,
|
|
||||||
userConnection,
|
|
||||||
anotherUser,
|
|
||||||
anotherUserConnection;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
userRole = await createRole({ name: 'User' });
|
|
||||||
anotherUser = await createUser({ roleId: userRole.id });
|
|
||||||
user = await createUser({ roleId: userRole.id });
|
|
||||||
|
|
||||||
userConnection = await createConnection({
|
|
||||||
key: 'deepl',
|
|
||||||
userId: user.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
anotherUserConnection = await createConnection({
|
|
||||||
key: 'deepl',
|
|
||||||
userId: anotherUser.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
await createPermission({
|
|
||||||
roleId: userRole.id,
|
|
||||||
action: 'read',
|
|
||||||
subject: 'Connection',
|
|
||||||
conditions: ['isCreator'],
|
|
||||||
});
|
|
||||||
|
|
||||||
step = await createStep();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should update step with the given payload and mark it as incomplete', async () => {
|
|
||||||
const stepData = {
|
|
||||||
appKey: 'deepl',
|
|
||||||
key: 'translateText',
|
|
||||||
connectionId: anotherUserConnection.id,
|
|
||||||
parameters: {
|
|
||||||
key: 'value',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const anotherUserWithRoleAndPermissions = await anotherUser
|
|
||||||
.$query()
|
|
||||||
.withGraphFetched({ permissions: true, role: true });
|
|
||||||
|
|
||||||
const updatedStep = await step.updateFor(
|
|
||||||
anotherUserWithRoleAndPermissions,
|
|
||||||
stepData
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(updatedStep).toMatchObject({
|
|
||||||
...stepData,
|
|
||||||
status: 'incomplete',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should invoke updateWebhookUrl', async () => {
|
|
||||||
const updateWebhookUrlSpy = vi
|
|
||||||
.spyOn(Step.prototype, 'updateWebhookUrl')
|
|
||||||
.mockResolvedValue();
|
|
||||||
|
|
||||||
const stepData = {
|
|
||||||
appKey: 'deepl',
|
|
||||||
key: 'translateText',
|
|
||||||
};
|
|
||||||
|
|
||||||
await step.updateFor(user, stepData);
|
|
||||||
|
|
||||||
expect(updateWebhookUrlSpy).toHaveBeenCalledOnce();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not update step when inaccessible connection is given', async () => {
|
|
||||||
const stepData = {
|
|
||||||
appKey: 'deepl',
|
|
||||||
key: 'translateText',
|
|
||||||
connectionId: userConnection.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
const anotherUserWithRoleAndPermissions = await anotherUser
|
|
||||||
.$query()
|
|
||||||
.withGraphFetched({ permissions: true, role: true });
|
|
||||||
|
|
||||||
await expect(() =>
|
|
||||||
step.updateFor(anotherUserWithRoleAndPermissions, stepData)
|
|
||||||
).rejects.toThrowError('NotFoundError');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not update step when given app key and key do not exist', async () => {
|
|
||||||
const stepData = {
|
|
||||||
appKey: 'deepl',
|
|
||||||
key: 'not-existing-key',
|
|
||||||
};
|
|
||||||
|
|
||||||
await expect(() => step.updateFor(user, stepData)).rejects.toThrowError(
|
|
||||||
'DeepL does not have an action with the "not-existing-key" key!'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('$afterInsert', () => {
|
|
||||||
it('should call super.$afterInsert', async () => {
|
|
||||||
const superAfterInsertSpy = vi.spyOn(Base.prototype, '$afterInsert');
|
|
||||||
|
|
||||||
await createStep();
|
|
||||||
|
|
||||||
expect(superAfterInsertSpy).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call Telemetry.stepCreated', async () => {
|
|
||||||
const telemetryStepCreatedSpy = vi
|
|
||||||
.spyOn(Telemetry, 'stepCreated')
|
|
||||||
.mockImplementation(() => {});
|
|
||||||
|
|
||||||
const step = await createStep();
|
|
||||||
|
|
||||||
expect(telemetryStepCreatedSpy).toHaveBeenCalledWith(step);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('$afterUpdate', () => {
|
|
||||||
it('should call super.$afterUpdate', async () => {
|
|
||||||
const superAfterUpdateSpy = vi.spyOn(Base.prototype, '$afterUpdate');
|
|
||||||
|
|
||||||
const step = await createStep();
|
|
||||||
|
|
||||||
await step.$query().patch({ position: 2 });
|
|
||||||
|
|
||||||
expect(superAfterUpdateSpy).toHaveBeenCalledOnce();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('$afterUpdate should call Telemetry.stepUpdated', async () => {
|
|
||||||
const telemetryStepUpdatedSpy = vi
|
|
||||||
.spyOn(Telemetry, 'stepUpdated')
|
|
||||||
.mockImplementation(() => {});
|
|
||||||
|
|
||||||
const step = await createStep();
|
|
||||||
|
|
||||||
await step.$query().patch({ position: 2 });
|
|
||||||
|
|
||||||
expect(telemetryStepUpdatedSpy).toHaveBeenCalled({});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@@ -212,10 +212,6 @@ class User extends Base {
|
|||||||
return `${appConfig.webAppUrl}/accept-invitation?token=${this.invitationToken}`;
|
return `${appConfig.webAppUrl}/accept-invitation?token=${this.invitationToken}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
get ability() {
|
|
||||||
return userAbility(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
static async authenticate(email, password) {
|
static async authenticate(email, password) {
|
||||||
const user = await User.query().findOne({
|
const user = await User.query().findOne({
|
||||||
email: email?.toLowerCase() || null,
|
email: email?.toLowerCase() || null,
|
||||||
@@ -227,8 +223,8 @@ class User extends Base {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async login(password) {
|
login(password) {
|
||||||
return await bcrypt.compare(password, this.password);
|
return bcrypt.compare(password, this.password);
|
||||||
}
|
}
|
||||||
|
|
||||||
async generateResetPasswordToken() {
|
async generateResetPasswordToken() {
|
||||||
@@ -411,7 +407,7 @@ class User extends Base {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
startTrialPeriod() {
|
async startTrialPeriod() {
|
||||||
this.trialExpiryDate = DateTime.now().plus({ days: 30 }).toISODate();
|
this.trialExpiryDate = DateTime.now().plus({ days: 30 }).toISODate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -587,30 +583,32 @@ class User extends Base {
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
can(action, subject) {
|
async $beforeInsert(queryContext) {
|
||||||
const can = this.ability.can(action, subject);
|
await super.$beforeInsert(queryContext);
|
||||||
|
|
||||||
if (!can) throw new NotAuthorizedError('The user is not authorized!');
|
this.email = this.email.toLowerCase();
|
||||||
|
await this.generateHash();
|
||||||
|
|
||||||
const relevantRule = this.ability.relevantRuleFor(action, subject);
|
if (appConfig.isCloud) {
|
||||||
|
await this.startTrialPeriod();
|
||||||
const conditions = relevantRule?.conditions || [];
|
|
||||||
const conditionMap = Object.fromEntries(
|
|
||||||
conditions.map((condition) => [condition, true])
|
|
||||||
);
|
|
||||||
|
|
||||||
return conditionMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
lowercaseEmail() {
|
|
||||||
if (this.email) {
|
|
||||||
this.email = this.email.toLowerCase();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async createUsageData() {
|
async $beforeUpdate(opt, queryContext) {
|
||||||
|
await super.$beforeUpdate(opt, queryContext);
|
||||||
|
|
||||||
|
if (this.email) {
|
||||||
|
this.email = this.email.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.generateHash();
|
||||||
|
}
|
||||||
|
|
||||||
|
async $afterInsert(queryContext) {
|
||||||
|
await super.$afterInsert(queryContext);
|
||||||
|
|
||||||
if (appConfig.isCloud) {
|
if (appConfig.isCloud) {
|
||||||
return await this.$relatedQuery('usageData').insertAndFetch({
|
await this.$relatedQuery('usageData').insert({
|
||||||
userId: this.id,
|
userId: this.id,
|
||||||
consumedTaskCount: 0,
|
consumedTaskCount: 0,
|
||||||
nextResetAt: DateTime.now().plus({ days: 30 }).toISODate(),
|
nextResetAt: DateTime.now().plus({ days: 30 }).toISODate(),
|
||||||
@@ -618,10 +616,8 @@ class User extends Base {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async omitEnterprisePermissionsWithoutValidLicense() {
|
async $afterFind() {
|
||||||
if (await hasValidLicense()) {
|
if (await hasValidLicense()) return this;
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(this.permissions)) {
|
if (Array.isArray(this.permissions)) {
|
||||||
this.permissions = this.permissions.filter((permission) => {
|
this.permissions = this.permissions.filter((permission) => {
|
||||||
@@ -635,35 +631,35 @@ class User extends Base {
|
|||||||
return !restrictedSubjects.includes(permission.subject);
|
return !restrictedSubjects.includes(permission.subject);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
async $beforeInsert(queryContext) {
|
get ability() {
|
||||||
await super.$beforeInsert(queryContext);
|
return userAbility(this);
|
||||||
|
|
||||||
this.lowercaseEmail();
|
|
||||||
await this.generateHash();
|
|
||||||
|
|
||||||
if (appConfig.isCloud) {
|
|
||||||
this.startTrialPeriod();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async $beforeUpdate(opt, queryContext) {
|
can(action, subject) {
|
||||||
await super.$beforeUpdate(opt, queryContext);
|
const can = this.ability.can(action, subject);
|
||||||
|
|
||||||
this.lowercaseEmail();
|
if (!can) throw new NotAuthorizedError();
|
||||||
|
|
||||||
await this.generateHash();
|
const relevantRule = this.ability.relevantRuleFor(action, subject);
|
||||||
|
|
||||||
|
const conditions = relevantRule?.conditions || [];
|
||||||
|
const conditionMap = Object.fromEntries(
|
||||||
|
conditions.map((condition) => [condition, true])
|
||||||
|
);
|
||||||
|
|
||||||
|
return conditionMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
async $afterInsert(queryContext) {
|
cannot(action, subject) {
|
||||||
await super.$afterInsert(queryContext);
|
const cannot = this.ability.cannot(action, subject);
|
||||||
|
|
||||||
await this.createUsageData();
|
if (cannot) throw new NotAuthorizedError();
|
||||||
}
|
|
||||||
|
|
||||||
async $afterFind() {
|
return cannot;
|
||||||
await this.omitEnterprisePermissionsWithoutValidLicense();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,10 @@ const redisConnection = {
|
|||||||
|
|
||||||
const actionQueue = new Queue('action', redisConnection);
|
const actionQueue = new Queue('action', redisConnection);
|
||||||
|
|
||||||
|
process.on('SIGTERM', async () => {
|
||||||
|
await actionQueue.close();
|
||||||
|
});
|
||||||
|
|
||||||
actionQueue.on('error', (error) => {
|
actionQueue.on('error', (error) => {
|
||||||
if (error.code === CONNECTION_REFUSED) {
|
if (error.code === CONNECTION_REFUSED) {
|
||||||
logger.error(
|
logger.error(
|
||||||
|
@@ -11,6 +11,10 @@ const redisConnection = {
|
|||||||
|
|
||||||
const deleteUserQueue = new Queue('delete-user', redisConnection);
|
const deleteUserQueue = new Queue('delete-user', redisConnection);
|
||||||
|
|
||||||
|
process.on('SIGTERM', async () => {
|
||||||
|
await deleteUserQueue.close();
|
||||||
|
});
|
||||||
|
|
||||||
deleteUserQueue.on('error', (error) => {
|
deleteUserQueue.on('error', (error) => {
|
||||||
if (error.code === CONNECTION_REFUSED) {
|
if (error.code === CONNECTION_REFUSED) {
|
||||||
logger.error(
|
logger.error(
|
||||||
|
@@ -11,6 +11,10 @@ const redisConnection = {
|
|||||||
|
|
||||||
const emailQueue = new Queue('email', redisConnection);
|
const emailQueue = new Queue('email', redisConnection);
|
||||||
|
|
||||||
|
process.on('SIGTERM', async () => {
|
||||||
|
await emailQueue.close();
|
||||||
|
});
|
||||||
|
|
||||||
emailQueue.on('error', (error) => {
|
emailQueue.on('error', (error) => {
|
||||||
if (error.code === CONNECTION_REFUSED) {
|
if (error.code === CONNECTION_REFUSED) {
|
||||||
logger.error(
|
logger.error(
|
||||||
|
@@ -11,6 +11,10 @@ const redisConnection = {
|
|||||||
|
|
||||||
const flowQueue = new Queue('flow', redisConnection);
|
const flowQueue = new Queue('flow', redisConnection);
|
||||||
|
|
||||||
|
process.on('SIGTERM', async () => {
|
||||||
|
await flowQueue.close();
|
||||||
|
});
|
||||||
|
|
||||||
flowQueue.on('error', (error) => {
|
flowQueue.on('error', (error) => {
|
||||||
if (error.code === CONNECTION_REFUSED) {
|
if (error.code === CONNECTION_REFUSED) {
|
||||||
logger.error(
|
logger.error(
|
||||||
|
@@ -1,21 +0,0 @@
|
|||||||
import appConfig from '../config/app.js';
|
|
||||||
import actionQueue from './action.js';
|
|
||||||
import emailQueue from './email.js';
|
|
||||||
import flowQueue from './flow.js';
|
|
||||||
import triggerQueue from './trigger.js';
|
|
||||||
import deleteUserQueue from './delete-user.ee.js';
|
|
||||||
import removeCancelledSubscriptionsQueue from './remove-cancelled-subscriptions.ee.js';
|
|
||||||
|
|
||||||
const queues = [
|
|
||||||
actionQueue,
|
|
||||||
emailQueue,
|
|
||||||
flowQueue,
|
|
||||||
triggerQueue,
|
|
||||||
deleteUserQueue,
|
|
||||||
];
|
|
||||||
|
|
||||||
if (appConfig.isCloud) {
|
|
||||||
queues.push(removeCancelledSubscriptionsQueue);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default queues;
|
|
@@ -14,6 +14,10 @@ const removeCancelledSubscriptionsQueue = new Queue(
|
|||||||
redisConnection
|
redisConnection
|
||||||
);
|
);
|
||||||
|
|
||||||
|
process.on('SIGTERM', async () => {
|
||||||
|
await removeCancelledSubscriptionsQueue.close();
|
||||||
|
});
|
||||||
|
|
||||||
removeCancelledSubscriptionsQueue.on('error', (error) => {
|
removeCancelledSubscriptionsQueue.on('error', (error) => {
|
||||||
if (error.code === CONNECTION_REFUSED) {
|
if (error.code === CONNECTION_REFUSED) {
|
||||||
logger.error(
|
logger.error(
|
||||||
|
@@ -11,6 +11,10 @@ const redisConnection = {
|
|||||||
|
|
||||||
const triggerQueue = new Queue('trigger', redisConnection);
|
const triggerQueue = new Queue('trigger', redisConnection);
|
||||||
|
|
||||||
|
process.on('SIGTERM', async () => {
|
||||||
|
await triggerQueue.close();
|
||||||
|
});
|
||||||
|
|
||||||
triggerQueue.on('error', (error) => {
|
triggerQueue.on('error', (error) => {
|
||||||
if (error.code === CONNECTION_REFUSED) {
|
if (error.code === CONNECTION_REFUSED) {
|
||||||
logger.error(
|
logger.error(
|
||||||
|
@@ -1,8 +1,10 @@
|
|||||||
const appConfigSerializer = (appConfig) => {
|
const appConfigSerializer = (appConfig) => {
|
||||||
return {
|
return {
|
||||||
key: appConfig.key,
|
key: appConfig.key,
|
||||||
useOnlyPredefinedAuthClients: appConfig.useOnlyPredefinedAuthClients,
|
customConnectionAllowed: appConfig.customConnectionAllowed,
|
||||||
|
shared: appConfig.shared,
|
||||||
disabled: appConfig.disabled,
|
disabled: appConfig.disabled,
|
||||||
|
connectionAllowed: appConfig.connectionAllowed,
|
||||||
createdAt: appConfig.createdAt.getTime(),
|
createdAt: appConfig.createdAt.getTime(),
|
||||||
updatedAt: appConfig.updatedAt.getTime(),
|
updatedAt: appConfig.updatedAt.getTime(),
|
||||||
};
|
};
|
||||||
|
@@ -12,8 +12,10 @@ describe('appConfig serializer', () => {
|
|||||||
it('should return app config data', async () => {
|
it('should return app config data', async () => {
|
||||||
const expectedPayload = {
|
const expectedPayload = {
|
||||||
key: appConfig.key,
|
key: appConfig.key,
|
||||||
useOnlyPredefinedAuthClients: appConfig.useOnlyPredefinedAuthClients,
|
customConnectionAllowed: appConfig.customConnectionAllowed,
|
||||||
|
shared: appConfig.shared,
|
||||||
disabled: appConfig.disabled,
|
disabled: appConfig.disabled,
|
||||||
|
connectionAllowed: appConfig.connectionAllowed,
|
||||||
createdAt: appConfig.createdAt.getTime(),
|
createdAt: appConfig.createdAt.getTime(),
|
||||||
updatedAt: appConfig.updatedAt.getTime(),
|
updatedAt: appConfig.updatedAt.getTime(),
|
||||||
};
|
};
|
||||||
|
@@ -2,9 +2,7 @@ const authSerializer = (auth) => {
|
|||||||
return {
|
return {
|
||||||
fields: auth.fields,
|
fields: auth.fields,
|
||||||
authenticationSteps: auth.authenticationSteps,
|
authenticationSteps: auth.authenticationSteps,
|
||||||
sharedAuthenticationSteps: auth.sharedAuthenticationSteps,
|
|
||||||
reconnectionSteps: auth.reconnectionSteps,
|
reconnectionSteps: auth.reconnectionSteps,
|
||||||
sharedReconnectionSteps: auth.sharedReconnectionSteps,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -10,8 +10,6 @@ describe('authSerializer', () => {
|
|||||||
fields: auth.fields,
|
fields: auth.fields,
|
||||||
authenticationSteps: auth.authenticationSteps,
|
authenticationSteps: auth.authenticationSteps,
|
||||||
reconnectionSteps: auth.reconnectionSteps,
|
reconnectionSteps: auth.reconnectionSteps,
|
||||||
sharedAuthenticationSteps: auth.sharedAuthenticationSteps,
|
|
||||||
sharedReconnectionSteps: auth.sharedReconnectionSteps,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(authSerializer(auth)).toStrictEqual(expectedPayload);
|
expect(authSerializer(auth)).toStrictEqual(expectedPayload);
|
||||||
|
@@ -2,6 +2,7 @@ const connectionSerializer = (connection) => {
|
|||||||
return {
|
return {
|
||||||
id: connection.id,
|
id: connection.id,
|
||||||
key: connection.key,
|
key: connection.key,
|
||||||
|
reconnectable: connection.reconnectable,
|
||||||
appAuthClientId: connection.appAuthClientId,
|
appAuthClientId: connection.appAuthClientId,
|
||||||
formattedData: {
|
formattedData: {
|
||||||
screenName: connection.formattedData.screenName,
|
screenName: connection.formattedData.screenName,
|
||||||
|
@@ -13,6 +13,7 @@ describe('connectionSerializer', () => {
|
|||||||
const expectedPayload = {
|
const expectedPayload = {
|
||||||
id: connection.id,
|
id: connection.id,
|
||||||
key: connection.key,
|
key: connection.key,
|
||||||
|
reconnectable: connection.reconnectable,
|
||||||
appAuthClientId: connection.appAuthClientId,
|
appAuthClientId: connection.appAuthClientId,
|
||||||
formattedData: {
|
formattedData: {
|
||||||
screenName: connection.formattedData.screenName,
|
screenName: connection.formattedData.screenName,
|
||||||
|
@@ -26,7 +26,7 @@ const serializers = {
|
|||||||
Permission: permissionSerializer,
|
Permission: permissionSerializer,
|
||||||
AdminSamlAuthProvider: adminSamlAuthProviderSerializer,
|
AdminSamlAuthProvider: adminSamlAuthProviderSerializer,
|
||||||
SamlAuthProvider: samlAuthProviderSerializer,
|
SamlAuthProvider: samlAuthProviderSerializer,
|
||||||
RoleMapping: samlAuthProviderRoleMappingSerializer,
|
SamlAuthProvidersRoleMapping: samlAuthProviderRoleMappingSerializer,
|
||||||
AppAuthClient: appAuthClientSerializer,
|
AppAuthClient: appAuthClientSerializer,
|
||||||
AppConfig: appConfigSerializer,
|
AppConfig: appConfigSerializer,
|
||||||
Flow: flowSerializer,
|
Flow: flowSerializer,
|
||||||
|
@@ -1,22 +1,20 @@
|
|||||||
import * as Sentry from './helpers/sentry.ee.js';
|
import * as Sentry from './helpers/sentry.ee.js';
|
||||||
import process from 'node:process';
|
import appConfig from './config/app.js';
|
||||||
|
|
||||||
Sentry.init();
|
Sentry.init();
|
||||||
|
|
||||||
import './config/orm.js';
|
import './config/orm.js';
|
||||||
import './helpers/check-worker-readiness.js';
|
import './helpers/check-worker-readiness.js';
|
||||||
import queues from './queues/index.js';
|
import './workers/flow.js';
|
||||||
import workers from './workers/index.js';
|
import './workers/trigger.js';
|
||||||
|
import './workers/action.js';
|
||||||
|
import './workers/email.js';
|
||||||
|
import './workers/delete-user.ee.js';
|
||||||
|
|
||||||
process.on('SIGTERM', async () => {
|
if (appConfig.isCloud) {
|
||||||
for (const queue of queues) {
|
import('./workers/remove-cancelled-subscriptions.ee.js');
|
||||||
await queue.close();
|
import('./queues/remove-cancelled-subscriptions.ee.js');
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const worker of workers) {
|
|
||||||
await worker.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
import telemetry from './helpers/telemetry/index.js';
|
import telemetry from './helpers/telemetry/index.js';
|
||||||
|
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import { Worker } from 'bullmq';
|
import { Worker } from 'bullmq';
|
||||||
|
import process from 'node:process';
|
||||||
|
|
||||||
import * as Sentry from '../helpers/sentry.ee.js';
|
import * as Sentry from '../helpers/sentry.ee.js';
|
||||||
import redisConfig from '../config/redis.js';
|
import redisConfig from '../config/redis.js';
|
||||||
@@ -14,7 +15,7 @@ import delayAsMilliseconds from '../helpers/delay-as-milliseconds.js';
|
|||||||
|
|
||||||
const DEFAULT_DELAY_DURATION = 0;
|
const DEFAULT_DELAY_DURATION = 0;
|
||||||
|
|
||||||
const actionWorker = new Worker(
|
export const worker = new Worker(
|
||||||
'action',
|
'action',
|
||||||
async (job) => {
|
async (job) => {
|
||||||
const { stepId, flowId, executionId, computedParameters, executionStep } =
|
const { stepId, flowId, executionId, computedParameters, executionStep } =
|
||||||
@@ -54,11 +55,11 @@ const actionWorker = new Worker(
|
|||||||
{ connection: redisConfig }
|
{ connection: redisConfig }
|
||||||
);
|
);
|
||||||
|
|
||||||
actionWorker.on('completed', (job) => {
|
worker.on('completed', (job) => {
|
||||||
logger.info(`JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has started!`);
|
logger.info(`JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has started!`);
|
||||||
});
|
});
|
||||||
|
|
||||||
actionWorker.on('failed', (job, err) => {
|
worker.on('failed', (job, err) => {
|
||||||
const errorMessage = `
|
const errorMessage = `
|
||||||
JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has failed to start with ${err.message}
|
JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has failed to start with ${err.message}
|
||||||
\n ${err.stack}
|
\n ${err.stack}
|
||||||
@@ -73,4 +74,6 @@ actionWorker.on('failed', (job, err) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
export default actionWorker;
|
process.on('SIGTERM', async () => {
|
||||||
|
await worker.close();
|
||||||
|
});
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import { Worker } from 'bullmq';
|
import { Worker } from 'bullmq';
|
||||||
|
import process from 'node:process';
|
||||||
|
|
||||||
import * as Sentry from '../helpers/sentry.ee.js';
|
import * as Sentry from '../helpers/sentry.ee.js';
|
||||||
import redisConfig from '../config/redis.js';
|
import redisConfig from '../config/redis.js';
|
||||||
@@ -7,7 +8,7 @@ import appConfig from '../config/app.js';
|
|||||||
import User from '../models/user.js';
|
import User from '../models/user.js';
|
||||||
import ExecutionStep from '../models/execution-step.js';
|
import ExecutionStep from '../models/execution-step.js';
|
||||||
|
|
||||||
const deleteUserWorker = new Worker(
|
export const worker = new Worker(
|
||||||
'delete-user',
|
'delete-user',
|
||||||
async (job) => {
|
async (job) => {
|
||||||
const { id } = job.data;
|
const { id } = job.data;
|
||||||
@@ -45,13 +46,13 @@ const deleteUserWorker = new Worker(
|
|||||||
{ connection: redisConfig }
|
{ connection: redisConfig }
|
||||||
);
|
);
|
||||||
|
|
||||||
deleteUserWorker.on('completed', (job) => {
|
worker.on('completed', (job) => {
|
||||||
logger.info(
|
logger.info(
|
||||||
`JOB ID: ${job.id} - The user with the ID of '${job.data.id}' has been deleted!`
|
`JOB ID: ${job.id} - The user with the ID of '${job.data.id}' has been deleted!`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
deleteUserWorker.on('failed', (job, err) => {
|
worker.on('failed', (job, err) => {
|
||||||
const errorMessage = `
|
const errorMessage = `
|
||||||
JOB ID: ${job.id} - The user with the ID of '${job.data.id}' has failed to be deleted! ${err.message}
|
JOB ID: ${job.id} - The user with the ID of '${job.data.id}' has failed to be deleted! ${err.message}
|
||||||
\n ${err.stack}
|
\n ${err.stack}
|
||||||
@@ -66,4 +67,6 @@ deleteUserWorker.on('failed', (job, err) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
export default deleteUserWorker;
|
process.on('SIGTERM', async () => {
|
||||||
|
await worker.close();
|
||||||
|
});
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import { Worker } from 'bullmq';
|
import { Worker } from 'bullmq';
|
||||||
|
import process from 'node:process';
|
||||||
|
|
||||||
import * as Sentry from '../helpers/sentry.ee.js';
|
import * as Sentry from '../helpers/sentry.ee.js';
|
||||||
import redisConfig from '../config/redis.js';
|
import redisConfig from '../config/redis.js';
|
||||||
@@ -15,7 +16,7 @@ const isAutomatischEmail = (email) => {
|
|||||||
return email.endsWith('@automatisch.io');
|
return email.endsWith('@automatisch.io');
|
||||||
};
|
};
|
||||||
|
|
||||||
const emailWorker = new Worker(
|
export const worker = new Worker(
|
||||||
'email',
|
'email',
|
||||||
async (job) => {
|
async (job) => {
|
||||||
const { email, subject, template, params } = job.data;
|
const { email, subject, template, params } = job.data;
|
||||||
@@ -38,13 +39,13 @@ const emailWorker = new Worker(
|
|||||||
{ connection: redisConfig }
|
{ connection: redisConfig }
|
||||||
);
|
);
|
||||||
|
|
||||||
emailWorker.on('completed', (job) => {
|
worker.on('completed', (job) => {
|
||||||
logger.info(
|
logger.info(
|
||||||
`JOB ID: ${job.id} - ${job.data.subject} email sent to ${job.data.email}!`
|
`JOB ID: ${job.id} - ${job.data.subject} email sent to ${job.data.email}!`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
emailWorker.on('failed', (job, err) => {
|
worker.on('failed', (job, err) => {
|
||||||
const errorMessage = `
|
const errorMessage = `
|
||||||
JOB ID: ${job.id} - ${job.data.subject} email to ${job.data.email} has failed to send with ${err.message}
|
JOB ID: ${job.id} - ${job.data.subject} email to ${job.data.email} has failed to send with ${err.message}
|
||||||
\n ${err.stack}
|
\n ${err.stack}
|
||||||
@@ -59,4 +60,6 @@ emailWorker.on('failed', (job, err) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
export default emailWorker;
|
process.on('SIGTERM', async () => {
|
||||||
|
await worker.close();
|
||||||
|
});
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import { Worker } from 'bullmq';
|
import { Worker } from 'bullmq';
|
||||||
|
import process from 'node:process';
|
||||||
|
|
||||||
import * as Sentry from '../helpers/sentry.ee.js';
|
import * as Sentry from '../helpers/sentry.ee.js';
|
||||||
import redisConfig from '../config/redis.js';
|
import redisConfig from '../config/redis.js';
|
||||||
@@ -12,7 +13,7 @@ import {
|
|||||||
REMOVE_AFTER_7_DAYS_OR_50_JOBS,
|
REMOVE_AFTER_7_DAYS_OR_50_JOBS,
|
||||||
} from '../helpers/remove-job-configuration.js';
|
} from '../helpers/remove-job-configuration.js';
|
||||||
|
|
||||||
const flowWorker = new Worker(
|
export const worker = new Worker(
|
||||||
'flow',
|
'flow',
|
||||||
async (job) => {
|
async (job) => {
|
||||||
const { flowId } = job.data;
|
const { flowId } = job.data;
|
||||||
@@ -63,11 +64,11 @@ const flowWorker = new Worker(
|
|||||||
{ connection: redisConfig }
|
{ connection: redisConfig }
|
||||||
);
|
);
|
||||||
|
|
||||||
flowWorker.on('completed', (job) => {
|
worker.on('completed', (job) => {
|
||||||
logger.info(`JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has started!`);
|
logger.info(`JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has started!`);
|
||||||
});
|
});
|
||||||
|
|
||||||
flowWorker.on('failed', async (job, err) => {
|
worker.on('failed', async (job, err) => {
|
||||||
const errorMessage = `
|
const errorMessage = `
|
||||||
JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has failed to start with ${err.message}
|
JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has failed to start with ${err.message}
|
||||||
\n ${err.stack}
|
\n ${err.stack}
|
||||||
@@ -94,4 +95,6 @@ flowWorker.on('failed', async (job, err) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
export default flowWorker;
|
process.on('SIGTERM', async () => {
|
||||||
|
await worker.close();
|
||||||
|
});
|
||||||
|
@@ -1,21 +0,0 @@
|
|||||||
import appConfig from '../config/app.js';
|
|
||||||
import actionWorker from './action.js';
|
|
||||||
import emailWorker from './email.js';
|
|
||||||
import flowWorker from './flow.js';
|
|
||||||
import triggerWorker from './trigger.js';
|
|
||||||
import deleteUserWorker from './delete-user.ee.js';
|
|
||||||
import removeCancelledSubscriptionsWorker from './remove-cancelled-subscriptions.ee.js';
|
|
||||||
|
|
||||||
const workers = [
|
|
||||||
actionWorker,
|
|
||||||
emailWorker,
|
|
||||||
flowWorker,
|
|
||||||
triggerWorker,
|
|
||||||
deleteUserWorker,
|
|
||||||
];
|
|
||||||
|
|
||||||
if (appConfig.isCloud) {
|
|
||||||
workers.push(removeCancelledSubscriptionsWorker);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default workers;
|
|
@@ -1,11 +1,12 @@
|
|||||||
import { Worker } from 'bullmq';
|
import { Worker } from 'bullmq';
|
||||||
|
import process from 'node:process';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import * as Sentry from '../helpers/sentry.ee.js';
|
import * as Sentry from '../helpers/sentry.ee.js';
|
||||||
import redisConfig from '../config/redis.js';
|
import redisConfig from '../config/redis.js';
|
||||||
import logger from '../helpers/logger.js';
|
import logger from '../helpers/logger.js';
|
||||||
import Subscription from '../models/subscription.ee.js';
|
import Subscription from '../models/subscription.ee.js';
|
||||||
|
|
||||||
const removeCancelledSubscriptionsWorker = new Worker(
|
export const worker = new Worker(
|
||||||
'remove-cancelled-subscriptions',
|
'remove-cancelled-subscriptions',
|
||||||
async () => {
|
async () => {
|
||||||
await Subscription.query()
|
await Subscription.query()
|
||||||
@@ -22,13 +23,13 @@ const removeCancelledSubscriptionsWorker = new Worker(
|
|||||||
{ connection: redisConfig }
|
{ connection: redisConfig }
|
||||||
);
|
);
|
||||||
|
|
||||||
removeCancelledSubscriptionsWorker.on('completed', (job) => {
|
worker.on('completed', (job) => {
|
||||||
logger.info(
|
logger.info(
|
||||||
`JOB ID: ${job.id} - The cancelled subscriptions have been removed!`
|
`JOB ID: ${job.id} - The cancelled subscriptions have been removed!`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
removeCancelledSubscriptionsWorker.on('failed', (job, err) => {
|
worker.on('failed', (job, err) => {
|
||||||
const errorMessage = `
|
const errorMessage = `
|
||||||
JOB ID: ${job.id} - ERROR: The cancelled subscriptions can not be removed! ${err.message}
|
JOB ID: ${job.id} - ERROR: The cancelled subscriptions can not be removed! ${err.message}
|
||||||
\n ${err.stack}
|
\n ${err.stack}
|
||||||
@@ -41,4 +42,6 @@ removeCancelledSubscriptionsWorker.on('failed', (job, err) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
export default removeCancelledSubscriptionsWorker;
|
process.on('SIGTERM', async () => {
|
||||||
|
await worker.close();
|
||||||
|
});
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import { Worker } from 'bullmq';
|
import { Worker } from 'bullmq';
|
||||||
|
import process from 'node:process';
|
||||||
|
|
||||||
import * as Sentry from '../helpers/sentry.ee.js';
|
import * as Sentry from '../helpers/sentry.ee.js';
|
||||||
import redisConfig from '../config/redis.js';
|
import redisConfig from '../config/redis.js';
|
||||||
@@ -11,7 +12,7 @@ import {
|
|||||||
REMOVE_AFTER_7_DAYS_OR_50_JOBS,
|
REMOVE_AFTER_7_DAYS_OR_50_JOBS,
|
||||||
} from '../helpers/remove-job-configuration.js';
|
} from '../helpers/remove-job-configuration.js';
|
||||||
|
|
||||||
const triggerWorker = new Worker(
|
export const worker = new Worker(
|
||||||
'trigger',
|
'trigger',
|
||||||
async (job) => {
|
async (job) => {
|
||||||
const { flowId, executionId, stepId, executionStep } = await processTrigger(
|
const { flowId, executionId, stepId, executionStep } = await processTrigger(
|
||||||
@@ -40,11 +41,11 @@ const triggerWorker = new Worker(
|
|||||||
{ connection: redisConfig }
|
{ connection: redisConfig }
|
||||||
);
|
);
|
||||||
|
|
||||||
triggerWorker.on('completed', (job) => {
|
worker.on('completed', (job) => {
|
||||||
logger.info(`JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has started!`);
|
logger.info(`JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has started!`);
|
||||||
});
|
});
|
||||||
|
|
||||||
triggerWorker.on('failed', (job, err) => {
|
worker.on('failed', (job, err) => {
|
||||||
const errorMessage = `
|
const errorMessage = `
|
||||||
JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has failed to start with ${err.message}
|
JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has failed to start with ${err.message}
|
||||||
\n ${err.stack}
|
\n ${err.stack}
|
||||||
@@ -59,4 +60,6 @@ triggerWorker.on('failed', (job, err) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
export default triggerWorker;
|
process.on('SIGTERM', async () => {
|
||||||
|
await worker.close();
|
||||||
|
});
|
||||||
|
@@ -1,15 +1,16 @@
|
|||||||
import { faker } from '@faker-js/faker';
|
|
||||||
import { createRole } from './role.js';
|
import { createRole } from './role.js';
|
||||||
import RoleMapping from '../../src/models/role-mapping.ee.js';
|
|
||||||
import { createSamlAuthProvider } from './saml-auth-provider.ee.js';
|
import { createSamlAuthProvider } from './saml-auth-provider.ee.js';
|
||||||
|
import SamlAuthProviderRoleMapping from '../../src/models/saml-auth-providers-role-mapping.ee.js';
|
||||||
|
|
||||||
export const createRoleMapping = async (params = {}) => {
|
export const createRoleMapping = async (params = {}) => {
|
||||||
params.roleId = params.roleId || (await createRole()).id;
|
params.roleId = params?.roleId || (await createRole()).id;
|
||||||
params.samlAuthProviderId =
|
params.samlAuthProviderId =
|
||||||
params.samlAuthProviderId || (await createSamlAuthProvider()).id;
|
params?.samlAuthProviderId || (await createSamlAuthProvider()).id;
|
||||||
params.remoteRoleName = params.remoteRoleName || faker.person.jobType();
|
|
||||||
|
|
||||||
const roleMapping = await RoleMapping.query().insertAndFetch(params);
|
params.remoteRoleName = params?.remoteRoleName || 'User';
|
||||||
|
|
||||||
return roleMapping;
|
const samlAuthProviderRoleMapping =
|
||||||
|
await SamlAuthProviderRoleMapping.query().insertAndFetch(params);
|
||||||
|
|
||||||
|
return samlAuthProviderRoleMapping;
|
||||||
};
|
};
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
import Role from '../../src/models/role.js';
|
import Role from '../../src/models/role';
|
||||||
|
|
||||||
export const createRole = async (params = {}) => {
|
export const createRole = async (params = {}) => {
|
||||||
const name = faker.lorem.word();
|
const name = faker.lorem.word();
|
||||||
|
@@ -0,0 +1,16 @@
|
|||||||
|
import { faker } from '@faker-js/faker';
|
||||||
|
import { createRole } from './role.js';
|
||||||
|
import SamlAuthProvidersRoleMapping from '../../src/models/saml-auth-providers-role-mapping.ee.js';
|
||||||
|
import { createSamlAuthProvider } from './saml-auth-provider.ee.js';
|
||||||
|
|
||||||
|
export const createSamlAuthProvidersRoleMapping = async (params = {}) => {
|
||||||
|
params.roleId = params.roleId || (await createRole()).id;
|
||||||
|
params.samlAuthProviderId =
|
||||||
|
params.samlAuthProviderId || (await createSamlAuthProvider()).id;
|
||||||
|
params.remoteRoleName = params.remoteRoleName || faker.person.jobType();
|
||||||
|
|
||||||
|
const samlAuthProvider =
|
||||||
|
await SamlAuthProvidersRoleMapping.query().insertAndFetch(params);
|
||||||
|
|
||||||
|
return samlAuthProvider;
|
||||||
|
};
|
@@ -2,7 +2,8 @@ const createAppConfigMock = (appConfig) => {
|
|||||||
return {
|
return {
|
||||||
data: {
|
data: {
|
||||||
key: appConfig.key,
|
key: appConfig.key,
|
||||||
useOnlyPredefinedAuthClients: appConfig.useOnlyPredefinedAuthClients,
|
customConnectionAllowed: appConfig.customConnectionAllowed,
|
||||||
|
shared: appConfig.shared,
|
||||||
disabled: appConfig.disabled,
|
disabled: appConfig.disabled,
|
||||||
},
|
},
|
||||||
meta: {
|
meta: {
|
||||||
|
@@ -15,7 +15,7 @@ const getRoleMappingsMock = async (roleMappings) => {
|
|||||||
currentPage: null,
|
currentPage: null,
|
||||||
isArray: true,
|
isArray: true,
|
||||||
totalPages: null,
|
totalPages: null,
|
||||||
type: 'RoleMapping',
|
type: 'SamlAuthProvidersRoleMapping',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@@ -15,7 +15,7 @@ const createRoleMappingsMock = async (roleMappings) => {
|
|||||||
currentPage: null,
|
currentPage: null,
|
||||||
isArray: true,
|
isArray: true,
|
||||||
totalPages: null,
|
totalPages: null,
|
||||||
type: 'RoleMapping',
|
type: 'SamlAuthProvidersRoleMapping',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@@ -2,6 +2,7 @@ const createConnection = (connection) => {
|
|||||||
const connectionData = {
|
const connectionData = {
|
||||||
id: connection.id,
|
id: connection.id,
|
||||||
key: connection.key,
|
key: connection.key,
|
||||||
|
reconnectable: connection.reconnectable || true,
|
||||||
appAuthClientId: connection.appAuthClientId,
|
appAuthClientId: connection.appAuthClientId,
|
||||||
formattedData: connection.formattedData,
|
formattedData: connection.formattedData,
|
||||||
verified: connection.verified || false,
|
verified: connection.verified || false,
|
||||||
|
@@ -4,8 +4,6 @@ const getAuthMock = (auth) => {
|
|||||||
fields: auth.fields,
|
fields: auth.fields,
|
||||||
authenticationSteps: auth.authenticationSteps,
|
authenticationSteps: auth.authenticationSteps,
|
||||||
reconnectionSteps: auth.reconnectionSteps,
|
reconnectionSteps: auth.reconnectionSteps,
|
||||||
sharedReconnectionSteps: auth.sharedReconnectionSteps,
|
|
||||||
sharedAuthenticationSteps: auth.sharedAuthenticationSteps,
|
|
||||||
},
|
},
|
||||||
meta: {
|
meta: {
|
||||||
count: 1,
|
count: 1,
|
||||||
|
@@ -2,8 +2,10 @@ const getAppConfigMock = (appConfig) => {
|
|||||||
return {
|
return {
|
||||||
data: {
|
data: {
|
||||||
key: appConfig.key,
|
key: appConfig.key,
|
||||||
useOnlyPredefinedAuthClients: appConfig.useOnlyPredefinedAuthClients,
|
customConnectionAllowed: appConfig.customConnectionAllowed,
|
||||||
|
shared: appConfig.shared,
|
||||||
disabled: appConfig.disabled,
|
disabled: appConfig.disabled,
|
||||||
|
connectionAllowed: appConfig.connectionAllowed,
|
||||||
createdAt: appConfig.createdAt.getTime(),
|
createdAt: appConfig.createdAt.getTime(),
|
||||||
updatedAt: appConfig.updatedAt.getTime(),
|
updatedAt: appConfig.updatedAt.getTime(),
|
||||||
},
|
},
|
||||||
|
@@ -3,6 +3,7 @@ const getConnectionsMock = (connections) => {
|
|||||||
data: connections.map((connection) => ({
|
data: connections.map((connection) => ({
|
||||||
id: connection.id,
|
id: connection.id,
|
||||||
key: connection.key,
|
key: connection.key,
|
||||||
|
reconnectable: connection.reconnectable,
|
||||||
verified: connection.verified,
|
verified: connection.verified,
|
||||||
appAuthClientId: connection.appAuthClientId,
|
appAuthClientId: connection.appAuthClientId,
|
||||||
formattedData: {
|
formattedData: {
|
||||||
|
@@ -3,6 +3,7 @@ const resetConnectionMock = (connection) => {
|
|||||||
id: connection.id,
|
id: connection.id,
|
||||||
key: connection.key,
|
key: connection.key,
|
||||||
verified: connection.verified,
|
verified: connection.verified,
|
||||||
|
reconnectable: connection.reconnectable,
|
||||||
appAuthClientId: connection.appAuthClientId,
|
appAuthClientId: connection.appAuthClientId,
|
||||||
formattedData: {
|
formattedData: {
|
||||||
screenName: connection.formattedData.screenName,
|
screenName: connection.formattedData.screenName,
|
||||||
|
@@ -3,6 +3,7 @@ const updateConnectionMock = (connection) => {
|
|||||||
id: connection.id,
|
id: connection.id,
|
||||||
key: connection.key,
|
key: connection.key,
|
||||||
verified: connection.verified,
|
verified: connection.verified,
|
||||||
|
reconnectable: connection.reconnectable,
|
||||||
appAuthClientId: connection.appAuthClientId,
|
appAuthClientId: connection.appAuthClientId,
|
||||||
formattedData: {
|
formattedData: {
|
||||||
screenName: connection.formattedData.screenName,
|
screenName: connection.formattedData.screenName,
|
||||||
|
@@ -6,6 +6,18 @@ const createFlowMock = async (flow) => {
|
|||||||
status: flow.status,
|
status: flow.status,
|
||||||
createdAt: flow.createdAt.getTime(),
|
createdAt: flow.createdAt.getTime(),
|
||||||
updatedAt: flow.updatedAt.getTime(),
|
updatedAt: flow.updatedAt.getTime(),
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
position: 1,
|
||||||
|
status: 'incomplete',
|
||||||
|
type: 'trigger',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
position: 2,
|
||||||
|
status: 'incomplete',
|
||||||
|
type: 'action',
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@@ -3,6 +3,7 @@ const getConnectionMock = async (connection) => {
|
|||||||
id: connection.id,
|
id: connection.id,
|
||||||
key: connection.key,
|
key: connection.key,
|
||||||
verified: connection.verified,
|
verified: connection.verified,
|
||||||
|
reconnectable: connection.reconnectable,
|
||||||
appAuthClientId: connection.appAuthClientId,
|
appAuthClientId: connection.appAuthClientId,
|
||||||
formattedData: {
|
formattedData: {
|
||||||
screenName: connection.formattedData.screenName,
|
screenName: connection.formattedData.screenName,
|
||||||
|
@@ -2,25 +2,8 @@ import { defineConfig } from 'vitest/config';
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
root: './',
|
|
||||||
environment: 'node',
|
environment: 'node',
|
||||||
setupFiles: ['./test/setup/global-hooks.js'],
|
setupFiles: ['./test/setup/global-hooks.js'],
|
||||||
globals: true,
|
globals: true,
|
||||||
reporters: process.env.GITHUB_ACTIONS ? ['dot', 'github-actions'] : ['dot'],
|
|
||||||
coverage: {
|
|
||||||
reportOnFailure: true,
|
|
||||||
provider: 'v8',
|
|
||||||
reportsDirectory: './coverage',
|
|
||||||
reporter: ['text', 'lcov'],
|
|
||||||
all: true,
|
|
||||||
include: ['**/src/models/**', '**/src/controllers/**'],
|
|
||||||
thresholds: {
|
|
||||||
autoUpdate: true,
|
|
||||||
statements: 95.16,
|
|
||||||
branches: 94.66,
|
|
||||||
functions: 97.65,
|
|
||||||
lines: 95.16,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
File diff suppressed because it is too large
Load Diff
1
packages/docs/.gitignore
vendored
1
packages/docs/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
pages/.vitepress/cache
|
|
@@ -4,7 +4,6 @@
|
|||||||
"license": "See LICENSE file",
|
"license": "See LICENSE file",
|
||||||
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
|
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vitepress dev pages --port 3002",
|
"dev": "vitepress dev pages --port 3002",
|
||||||
"build": "vitepress build pages",
|
"build": "vitepress build pages",
|
||||||
|
@@ -6,19 +6,11 @@ Clone main branch of Automatisch.
|
|||||||
git clone git@github.com:automatisch/automatisch.git
|
git clone git@github.com:automatisch/automatisch.git
|
||||||
```
|
```
|
||||||
|
|
||||||
Then, install the dependencies for both backend and web packages separately.
|
Then, install the dependencies.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd automatisch
|
cd automatisch
|
||||||
|
|
||||||
# Install backend dependencies
|
|
||||||
cd packages/backend
|
|
||||||
yarn install
|
yarn install
|
||||||
|
|
||||||
# Install web dependencies
|
|
||||||
cd packages/web
|
|
||||||
yarn install
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Backend
|
## Backend
|
||||||
@@ -61,14 +53,12 @@ yarn db:seed:user
|
|||||||
Start the main backend server.
|
Start the main backend server.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd packages/backend
|
|
||||||
yarn dev
|
yarn dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Start the worker server in another terminal tab.
|
Start the worker server in another terminal tab.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd packages/backend
|
|
||||||
yarn worker
|
yarn worker
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -94,7 +84,6 @@ It will automatically open [http://localhost:3001](http://localhost:3001) in you
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd packages/docs
|
cd packages/docs
|
||||||
yarn install
|
|
||||||
yarn dev
|
yarn dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
# Repository Structure
|
# Repository Structure
|
||||||
|
|
||||||
We manage a monorepo structure with the following packages:
|
We use `lerna` with `yarn workspaces` to manage the mono repository. We have the following packages:
|
||||||
|
|
||||||
```
|
```
|
||||||
.
|
.
|
||||||
@@ -15,5 +15,3 @@ We manage a monorepo structure with the following packages:
|
|||||||
- `docs` - The docs package contains the documentation website.
|
- `docs` - The docs package contains the documentation website.
|
||||||
- `e2e-tests` - The e2e-tests package contains the end-to-end tests for the internal usage.
|
- `e2e-tests` - The e2e-tests package contains the end-to-end tests for the internal usage.
|
||||||
- `web` - The web package contains the frontend application of Automatisch.
|
- `web` - The web package contains the frontend application of Automatisch.
|
||||||
|
|
||||||
Each package is independently managed, and has its own package.json file to manage dependencies. This allows for better isolation and flexibility.
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -29,12 +29,10 @@
|
|||||||
"@playwright/test": "^1.45.1"
|
"@playwright/test": "^1.45.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.6.0",
|
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"eslint": "^8.13.0",
|
"eslint": "^8.13.0",
|
||||||
"eslint-config-prettier": "^8.3.0",
|
"eslint-config-prettier": "^8.3.0",
|
||||||
"eslint-plugin-prettier": "^4.0.0",
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
"knex": "^2.4.0",
|
|
||||||
"luxon": "^3.4.4",
|
"luxon": "^3.4.4",
|
||||||
"micro": "^10.0.1",
|
"micro": "^10.0.1",
|
||||||
"pg": "^8.12.0",
|
"pg": "^8.12.0",
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user