Compare commits
197 Commits
dependabot
...
AUT-1379
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a82dff5c79 | ||
![]() |
112baad65f | ||
![]() |
978ceaadb6 | ||
![]() |
770b07179f | ||
![]() |
6d15167ad9 | ||
![]() |
39cba6bc74 | ||
![]() |
9558e66abf | ||
![]() |
ff7908955e | ||
![]() |
26b095b835 | ||
![]() |
feba2a32f9 | ||
![]() |
5090ece9b6 | ||
![]() |
221b19586e | ||
![]() |
3346c14255 | ||
![]() |
6e97e023c9 | ||
![]() |
b26e2ecf2e | ||
![]() |
d896238f23 | ||
![]() |
d2c8f5a75c | ||
![]() |
ce430d238c | ||
![]() |
ee397441ed | ||
![]() |
ba82d986c1 | ||
![]() |
2361cb521e | ||
![]() |
05f8d95281 | ||
![]() |
6c60b1c263 | ||
![]() |
0c32a0693c | ||
![]() |
807faa3c93 | ||
![]() |
fb53e37f7a | ||
![]() |
4ffdf98e16 | ||
![]() |
b8da721e39 | ||
![]() |
db8b98ca16 | ||
![]() |
01b8c600fe | ||
![]() |
69bd5549a2 | ||
![]() |
bc631e3931 | ||
![]() |
8ca4bc5a33 | ||
![]() |
58a569afb0 | ||
![]() |
db718d6fc3 | ||
![]() |
ca9cb8b07b | ||
![]() |
ef14586412 | ||
![]() |
09335fcd79 | ||
![]() |
15f1fca6fe | ||
![]() |
a570b8eb7a | ||
![]() |
02e2735b7a | ||
![]() |
54fa347142 | ||
![]() |
0c752beace | ||
![]() |
c14f808d29 | ||
![]() |
ad71173671 | ||
![]() |
204325ef44 | ||
![]() |
7ce6117659 | ||
![]() |
823a2c8b73 | ||
![]() |
741866e742 | ||
![]() |
41622678b0 | ||
![]() |
449b953401 | ||
![]() |
551548400f | ||
![]() |
6345ce5195 | ||
![]() |
95651f6163 | ||
![]() |
b02c1545b7 | ||
![]() |
2deaab9b24 | ||
![]() |
f0d4853533 | ||
![]() |
af81ae812f | ||
![]() |
bae76064e5 | ||
![]() |
07d9198cc8 | ||
![]() |
a2e07ea2f7 | ||
![]() |
864c762fe2 | ||
![]() |
167bb4e8a0 | ||
![]() |
4cf64ede74 | ||
![]() |
bb309fea6f | ||
![]() |
90a7b4c1c0 | ||
![]() |
1133362028 | ||
![]() |
eb9226bd4a | ||
![]() |
a9abdcc37e | ||
![]() |
6ace93bdbf | ||
![]() |
b89197939a | ||
![]() |
da788106af | ||
![]() |
49e92e6f1d | ||
![]() |
a6c3276104 | ||
![]() |
6388bfc714 | ||
![]() |
bebc3b181d | ||
![]() |
5a6d561c1a | ||
![]() |
5ba575fdfd | ||
![]() |
dcf8bbd804 | ||
![]() |
ff93ffd0b1 | ||
![]() |
395c09df92 | ||
![]() |
4c903cd08b | ||
![]() |
64cb98717c | ||
![]() |
b0e4ce54fb | ||
![]() |
d67a37002f | ||
![]() |
965ff8bc3f | ||
![]() |
400a495ad2 | ||
![]() |
09d0822a8d | ||
![]() |
7016c20ccc | ||
![]() |
df54895805 | ||
![]() |
62d5e6fe51 | ||
![]() |
4615a0b7ea | ||
![]() |
280d603b14 | ||
![]() |
36271f0749 | ||
![]() |
579638f932 | ||
![]() |
48871c82a6 | ||
![]() |
14056c42ef | ||
![]() |
90fe1576de | ||
![]() |
d61cf13985 | ||
![]() |
dfe6dfd0c6 | ||
![]() |
c138c7d0e9 | ||
![]() |
d542be947e | ||
![]() |
c76366e72e | ||
![]() |
75abfda783 | ||
![]() |
f3d8d7d4ad | ||
![]() |
7255eccb22 | ||
![]() |
a0decb70cc | ||
![]() |
532f562495 | ||
![]() |
27e58ae925 | ||
![]() |
abf30dfc1a | ||
![]() |
218b8ce86e | ||
![]() |
4867ffcb4b | ||
![]() |
e34c3b411d | ||
![]() |
c91b8be1a6 | ||
![]() |
9cb41644a1 | ||
![]() |
8c01cea147 | ||
![]() |
58eb55e90a | ||
![]() |
bb05e82e15 | ||
![]() |
5ab95ea175 | ||
![]() |
a25c4f1d1e | ||
![]() |
15287de8af | ||
![]() |
49b4d6b511 | ||
![]() |
d5b4a5d4ac | ||
![]() |
de480b491c | ||
![]() |
a949fda1fc | ||
![]() |
3e28af670c | ||
![]() |
b5310afb90 | ||
![]() |
da81ecf915 | ||
![]() |
f597066d16 | ||
![]() |
ec30606b24 | ||
![]() |
20dce14f17 | ||
![]() |
821742de85 | ||
![]() |
74dc108f62 | ||
![]() |
a05fe856bb | ||
![]() |
d13f51a32d | ||
![]() |
3dbe599cb3 | ||
![]() |
cf966dd83c | ||
![]() |
4e62f3654f | ||
![]() |
970d926563 | ||
![]() |
ff49c747ba | ||
![]() |
c46b8a5f4f | ||
![]() |
485324e204 | ||
![]() |
4696a03db1 | ||
![]() |
7885de36a9 | ||
![]() |
fac4339207 | ||
![]() |
9c70519021 | ||
![]() |
9ae77ecd5d | ||
![]() |
1c8e6f278d | ||
![]() |
c0a190a9f2 | ||
![]() |
e29e2a62f0 | ||
![]() |
1580640a35 | ||
![]() |
33c84b7fcc | ||
![]() |
9773ce75b0 | ||
![]() |
c310e8d152 | ||
![]() |
af251c7b81 | ||
![]() |
122483de0c | ||
![]() |
42c2131144 | ||
![]() |
71bc7a62c2 | ||
![]() |
87bfff07db | ||
![]() |
1cb5b780d2 | ||
![]() |
2f6acd4d6e | ||
![]() |
c2e2351505 | ||
![]() |
d847b5480b | ||
![]() |
32749ee58e | ||
![]() |
a531b8b5fe | ||
![]() |
148a0c5bb0 | ||
![]() |
39f9a58200 | ||
![]() |
edd113d344 | ||
![]() |
c641e8729b | ||
![]() |
2c4b13e4b5 | ||
![]() |
48fcf4dda7 | ||
![]() |
acfd980d4f | ||
![]() |
db9bfab812 | ||
![]() |
d32820ee09 | ||
![]() |
0f823fd19e | ||
![]() |
4308ed5850 | ||
![]() |
b9cd7c3983 | ||
![]() |
fa607aa961 | ||
![]() |
6900b71841 | ||
![]() |
bb230d67e8 | ||
![]() |
4f076ec3e3 | ||
![]() |
96a6cbfb95 | ||
![]() |
5bdc5aed72 | ||
![]() |
d38b0f088b | ||
![]() |
892710f705 | ||
![]() |
fbf898be64 | ||
![]() |
e3e2ecc1e1 | ||
![]() |
b59807d221 | ||
![]() |
163ad52285 | ||
![]() |
4023a6d1cc | ||
![]() |
ec827e5dc0 | ||
![]() |
a8f4fb7c22 | ||
![]() |
bc195ed452 | ||
![]() |
41568904ab | ||
![]() |
79050af391 | ||
![]() |
2e5b44c424 | ||
![]() |
7a437660d1 |
@@ -5,8 +5,11 @@ 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
|
||||||
@@ -21,24 +24,35 @@ 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
|
||||||
|
|
||||||
echo "Done!"
|
echo "Done!"
|
||||||
|
9
.github/workflows/backend.yml
vendored
9
.github/workflows/backend.yml
vendored
@@ -41,8 +41,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 18
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: cd packages/backend && yarn
|
run: yarn
|
||||||
|
working-directory: packages/backend
|
||||||
- name: Copy .env-example.test file to .env.test
|
- name: Copy .env-example.test file to .env.test
|
||||||
run: cd packages/backend && cp .env-example.test .env.test
|
run: cp .env-example.test .env.test
|
||||||
|
working-directory: packages/backend
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: cd packages/backend && yarn test
|
run: yarn test:coverage
|
||||||
|
working-directory: packages/backend
|
||||||
|
30
.github/workflows/ci.yml
vendored
30
.github/workflows/ci.yml
vendored
@@ -18,11 +18,13 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '18'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
cache-dependency-path: yarn.lock
|
cache-dependency-path: packages/backend/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
|
||||||
- run: cd packages/backend && yarn lint
|
working-directory: packages/backend
|
||||||
|
- 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
|
||||||
@@ -35,11 +37,13 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '18'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
cache-dependency-path: yarn.lock
|
cache-dependency-path: packages/backend/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 && yarn lerna bootstrap
|
- run: yarn --frozen-lockfile
|
||||||
- run: cd packages/backend && yarn start
|
working-directory: packages/backend
|
||||||
|
- 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
|
||||||
@@ -55,11 +59,13 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '18'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
cache-dependency-path: yarn.lock
|
cache-dependency-path: packages/backend/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 && yarn lerna bootstrap
|
- run: yarn --frozen-lockfile
|
||||||
- run: cd packages/backend && yarn start:worker
|
working-directory: packages/backend
|
||||||
|
- 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
|
||||||
@@ -75,11 +81,13 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '18'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
cache-dependency-path: yarn.lock
|
cache-dependency-path: packages/web/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 && yarn lerna bootstrap
|
- run: yarn --frozen-lockfile
|
||||||
- run: cd packages/web && yarn build
|
working-directory: packages/web
|
||||||
|
- 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 }}."
|
||||||
|
27
.github/workflows/playwright.yml
vendored
27
.github/workflows/playwright.yml
vendored
@@ -3,12 +3,13 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
pull_request:
|
# TODO: Add pull request after optimizing the total excecution time of the test suite.
|
||||||
paths:
|
# pull_request:
|
||||||
- 'packages/backend/**'
|
# paths:
|
||||||
- 'packages/e2e-tests/**'
|
# - 'packages/backend/**'
|
||||||
- 'packages/web/**'
|
# - 'packages/e2e-tests/**'
|
||||||
- '!packages/backend/src/apps/**'
|
# - 'packages/web/**'
|
||||||
|
# - '!packages/backend/src/apps/**'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
@@ -58,13 +59,21 @@ jobs:
|
|||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 18
|
||||||
- name: Install dependencies
|
- name: Install web dependencies
|
||||||
run: yarn && yarn lerna bootstrap
|
run: yarn
|
||||||
|
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
|
||||||
working-directory: ./packages/web
|
|
||||||
run: yarn build
|
run: yarn build
|
||||||
|
working-directory: ./packages/web
|
||||||
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,7 +4,6 @@ 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,10 +11,12 @@ WORKDIR /automatisch
|
|||||||
# copy the app, note .dockerignore
|
# copy the app, note .dockerignore
|
||||||
COPY . /automatisch
|
COPY . /automatisch
|
||||||
|
|
||||||
RUN yarn
|
RUN cd packages/web && 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
13
lerna.json
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"packages": [
|
|
||||||
"packages/*"
|
|
||||||
],
|
|
||||||
"version": "0.10.0",
|
|
||||||
"npmClient": "yarn",
|
|
||||||
"useWorkspaces": true,
|
|
||||||
"command": {
|
|
||||||
"add": {
|
|
||||||
"exact": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
32
package.json
32
package.json
@@ -1,32 +0,0 @@
|
|||||||
{
|
|
||||||
"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,6 +12,7 @@
|
|||||||
"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",
|
||||||
@@ -23,6 +24,7 @@
|
|||||||
"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",
|
||||||
@@ -36,6 +38,9 @@
|
|||||||
"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",
|
||||||
@@ -61,6 +66,7 @@
|
|||||||
"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",
|
||||||
@@ -92,10 +98,11 @@
|
|||||||
"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": "^1.1.3"
|
"vitest": "^2.1.5"
|
||||||
},
|
},
|
||||||
"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: false,
|
required: true,
|
||||||
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.13.1',
|
version: '0.14.0',
|
||||||
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'),
|
||||||
|
@@ -32,7 +32,7 @@ describe('POST /api/v1/access-tokens', () => {
|
|||||||
})
|
})
|
||||||
.expect(422);
|
.expect(422);
|
||||||
|
|
||||||
expect(response.body.errors.general).toEqual([
|
expect(response.body.errors.general).toStrictEqual([
|
||||||
'Incorrect email or password.',
|
'Incorrect email or password.',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
@@ -83,7 +83,7 @@ describe('POST /api/v1/admin/apps/:appKey/auth-clients', () => {
|
|||||||
.send(appAuthClient)
|
.send(appAuthClient)
|
||||||
.expect(422);
|
.expect(422);
|
||||||
|
|
||||||
expect(response.body.meta.type).toEqual('ModelValidation');
|
expect(response.body.meta.type).toStrictEqual('ModelValidation');
|
||||||
expect(response.body.errors).toMatchObject({
|
expect(response.body.errors).toMatchObject({
|
||||||
name: ["must have required property 'name'"],
|
name: ["must have required property 'name'"],
|
||||||
formattedAuthDefaults: [
|
formattedAuthDefaults: [
|
||||||
|
@@ -59,7 +59,7 @@ describe('POST /api/v1/admin/apps/:appKey/config', () => {
|
|||||||
})
|
})
|
||||||
.expect(422);
|
.expect(422);
|
||||||
|
|
||||||
expect(response.body.meta.type).toEqual('UniqueViolationError');
|
expect(response.body.meta.type).toStrictEqual('UniqueViolationError');
|
||||||
expect(response.body.errors).toMatchObject({
|
expect(response.body.errors).toMatchObject({
|
||||||
key: ["'key' must be unique."],
|
key: ["'key' must be unique."],
|
||||||
});
|
});
|
||||||
|
@@ -32,7 +32,7 @@ describe('GET /api/v1/admin/apps/:appKey/auth-clients/:appAuthClientId', () => {
|
|||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
const expectedPayload = getAppAuthClientMock(currentAppAuthClient);
|
const expectedPayload = getAppAuthClientMock(currentAppAuthClient);
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return not found response for not existing app auth client ID', async () => {
|
it('should return not found response for not existing app auth client ID', async () => {
|
||||||
|
@@ -39,6 +39,6 @@ describe('GET /api/v1/admin/apps/:appKey/auth-clients', () => {
|
|||||||
appAuthClientOne,
|
appAuthClientOne,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -83,7 +83,7 @@ describe('PATCH /api/v1/admin/apps/:appKey/config', () => {
|
|||||||
})
|
})
|
||||||
.expect(422);
|
.expect(422);
|
||||||
|
|
||||||
expect(response.body.meta.type).toEqual('ModelValidation');
|
expect(response.body.meta.type).toStrictEqual('ModelValidation');
|
||||||
expect(response.body.errors).toMatchObject({
|
expect(response.body.errors).toMatchObject({
|
||||||
disabled: ['must be boolean'],
|
disabled: ['must be boolean'],
|
||||||
});
|
});
|
||||||
|
@@ -50,8 +50,8 @@ describe('PATCH /api/v1/admin/config', () => {
|
|||||||
.send(newConfigValues)
|
.send(newConfigValues)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(response.body.data.title).toEqual(newTitle);
|
expect(response.body.data.title).toStrictEqual(newTitle);
|
||||||
expect(response.body.meta.type).toEqual('Config');
|
expect(response.body.meta.type).toStrictEqual('Config');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return created config for unexisting config', async () => {
|
it('should return created config for unexisting config', async () => {
|
||||||
@@ -67,8 +67,8 @@ describe('PATCH /api/v1/admin/config', () => {
|
|||||||
.send(newConfigValues)
|
.send(newConfigValues)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(response.body.data.title).toEqual(newTitle);
|
expect(response.body.data.title).toStrictEqual(newTitle);
|
||||||
expect(response.body.meta.type).toEqual('Config');
|
expect(response.body.meta.type).toStrictEqual('Config');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return null for deleted config entry', async () => {
|
it('should return null for deleted config entry', async () => {
|
||||||
@@ -83,6 +83,6 @@ describe('PATCH /api/v1/admin/config', () => {
|
|||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(response.body.data.title).toBeNull();
|
expect(response.body.data.title).toBeNull();
|
||||||
expect(response.body.meta.type).toEqual('Config');
|
expect(response.body.meta.type).toStrictEqual('Config');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -27,6 +27,6 @@ describe('GET /api/v1/admin/permissions/catalog', () => {
|
|||||||
|
|
||||||
const expectedPayload = await getPermissionsCatalogMock();
|
const expectedPayload = await getPermissionsCatalogMock();
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -58,7 +58,7 @@ describe('POST /api/v1/admin/roles', () => {
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return unprocessable entity response for invalid role data', async () => {
|
it('should return unprocessable entity response for invalid role data', async () => {
|
||||||
|
@@ -92,21 +92,4 @@ describe('DELETE /api/v1/admin/roles/:roleId', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not delete role and permissions on unsuccessful response', async () => {
|
|
||||||
const role = await createRole();
|
|
||||||
const permission = await createPermission({ roleId: role.id });
|
|
||||||
await createUser({ roleId: role.id });
|
|
||||||
|
|
||||||
await request(app)
|
|
||||||
.delete(`/api/v1/admin/roles/${role.id}`)
|
|
||||||
.set('Authorization', token)
|
|
||||||
.expect(422);
|
|
||||||
|
|
||||||
const refetchedRole = await role.$query();
|
|
||||||
const refetchedPermission = await permission.$query();
|
|
||||||
|
|
||||||
expect(refetchedRole).toStrictEqual(role);
|
|
||||||
expect(refetchedPermission).toStrictEqual(permission);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@@ -34,7 +34,7 @@ describe('GET /api/v1/admin/roles/:roleId', () => {
|
|||||||
permissionTwo,
|
permissionTwo,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return not found response for not existing role UUID', async () => {
|
it('should return not found response for not existing role UUID', async () => {
|
||||||
|
@@ -28,6 +28,6 @@ describe('GET /api/v1/admin/roles', () => {
|
|||||||
|
|
||||||
const expectedPayload = await getRolesMock([roleOne, roleTwo]);
|
const expectedPayload = await getRolesMock([roleOne, roleTwo]);
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -7,7 +7,7 @@ export default async (request, response) => {
|
|||||||
.throwIfNotFound();
|
.throwIfNotFound();
|
||||||
|
|
||||||
const roleMappings = await samlAuthProvider
|
const roleMappings = await samlAuthProvider
|
||||||
.$relatedQuery('samlAuthProvidersRoleMappings')
|
.$relatedQuery('roleMappings')
|
||||||
.orderBy('remote_role_name', 'asc');
|
.orderBy('remote_role_name', 'asc');
|
||||||
|
|
||||||
renderObject(response, roleMappings);
|
renderObject(response, roleMappings);
|
||||||
|
@@ -46,6 +46,6 @@ describe('GET /api/v1/admin/saml-auth-providers/:samlAuthProviderId/role-mapping
|
|||||||
roleMappingTwo,
|
roleMappingTwo,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -30,7 +30,7 @@ describe('GET /api/v1/admin/saml-auth-provider/:samlAuthProviderId', () => {
|
|||||||
|
|
||||||
const expectedPayload = await getSamlAuthProviderMock(samlAuthProvider);
|
const expectedPayload = await getSamlAuthProviderMock(samlAuthProvider);
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return not found response for not existing saml auth provider UUID', async () => {
|
it('should return not found response for not existing saml auth provider UUID', async () => {
|
||||||
|
@@ -34,6 +34,6 @@ describe('GET /api/v1/admin/saml-auth-providers', () => {
|
|||||||
samlAuthProviderOne,
|
samlAuthProviderOne,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -8,15 +8,14 @@ export default async (request, response) => {
|
|||||||
.findById(samlAuthProviderId)
|
.findById(samlAuthProviderId)
|
||||||
.throwIfNotFound();
|
.throwIfNotFound();
|
||||||
|
|
||||||
const samlAuthProvidersRoleMappings =
|
const roleMappings = await samlAuthProvider.updateRoleMappings(
|
||||||
await samlAuthProvider.updateRoleMappings(
|
roleMappingsParams(request)
|
||||||
samlAuthProvidersRoleMappingsParams(request)
|
);
|
||||||
);
|
|
||||||
|
|
||||||
renderObject(response, samlAuthProvidersRoleMappings);
|
renderObject(response, roleMappings);
|
||||||
};
|
};
|
||||||
|
|
||||||
const samlAuthProvidersRoleMappingsParams = (request) => {
|
const roleMappingsParams = (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 { createSamlAuthProvidersRoleMapping } from '../../../../../../test/factories/saml-auth-providers-role-mapping.js';
|
import { createRoleMapping } from '../../../../../../test/factories/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 createSamlAuthProvidersRoleMapping({
|
await createRoleMapping({
|
||||||
samlAuthProviderId: samlAuthProvider.id,
|
samlAuthProviderId: samlAuthProvider.id,
|
||||||
remoteRoleName: 'Viewer',
|
remoteRoleName: 'Viewer',
|
||||||
});
|
});
|
||||||
|
|
||||||
await createSamlAuthProvidersRoleMapping({
|
await createRoleMapping({
|
||||||
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(
|
||||||
'samlAuthProvidersRoleMappings'
|
'roleMappings'
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(existingRoleMappings.length).toBe(2);
|
expect(existingRoleMappings.length).toBe(2);
|
||||||
@@ -149,34 +149,4 @@ 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);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@@ -30,7 +30,7 @@ describe('GET /api/v1/admin/users/:userId', () => {
|
|||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
const expectedPayload = getUserMock(anotherUser, anotherUserRole);
|
const expectedPayload = getUserMock(anotherUser, anotherUserRole);
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return not found response for not existing user UUID', async () => {
|
it('should return not found response for not existing user UUID', async () => {
|
||||||
|
@@ -40,6 +40,6 @@ describe('GET /api/v1/admin/users', () => {
|
|||||||
[anotherUserRole, currentUserRole]
|
[anotherUserRole, currentUserRole]
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedResponsePayload);
|
expect(response.body).toStrictEqual(expectedResponsePayload);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -61,7 +61,8 @@ describe('PATCH /api/v1/admin/users/:userId', () => {
|
|||||||
.send(anotherUserUpdatedData)
|
.send(anotherUserUpdatedData)
|
||||||
.expect(422);
|
.expect(422);
|
||||||
|
|
||||||
expect(response.body.meta.type).toEqual('ModelValidation');
|
expect(response.body.meta.type).toStrictEqual('ModelValidation');
|
||||||
|
|
||||||
expect(response.body.errors).toMatchObject({
|
expect(response.body.errors).toMatchObject({
|
||||||
email: ['must be string'],
|
email: ['must be string'],
|
||||||
fullName: ['must be string'],
|
fullName: ['must be string'],
|
||||||
|
@@ -29,7 +29,7 @@ describe('GET /api/v1/apps/:appKey/actions/:actionKey/substeps', () => {
|
|||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
const expectedPayload = getActionSubstepsMock(exampleAction.substeps);
|
const expectedPayload = getActionSubstepsMock(exampleAction.substeps);
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return not found response for invalid app key', async () => {
|
it('should return not found response for invalid app key', async () => {
|
||||||
@@ -47,6 +47,6 @@ describe('GET /api/v1/apps/:appKey/actions/:actionKey/substeps', () => {
|
|||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(response.body.data).toEqual([]);
|
expect(response.body.data).toStrictEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -23,7 +23,7 @@ describe('GET /api/v1/apps/:appKey/actions', () => {
|
|||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
const expectedPayload = getActionsMock(exampleApp.actions);
|
const expectedPayload = getActionsMock(exampleApp.actions);
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return not found response for invalid app key', async () => {
|
it('should return not found response for invalid app key', async () => {
|
||||||
|
@@ -23,7 +23,7 @@ describe('GET /api/v1/apps/:appKey', () => {
|
|||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
const expectedPayload = getAppMock(exampleApp);
|
const expectedPayload = getAppMock(exampleApp);
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return not found response for invalid app key', async () => {
|
it('should return not found response for invalid app key', async () => {
|
||||||
|
@@ -22,7 +22,7 @@ describe('GET /api/v1/apps', () => {
|
|||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
const expectedPayload = getAppsMock(apps);
|
const expectedPayload = getAppsMock(apps);
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return all apps filtered by name', async () => {
|
it('should return all apps filtered by name', async () => {
|
||||||
@@ -34,7 +34,7 @@ describe('GET /api/v1/apps', () => {
|
|||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
const expectedPayload = getAppsMock(appsWithNameGit);
|
const expectedPayload = getAppsMock(appsWithNameGit);
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return only the apps with triggers', async () => {
|
it('should return only the apps with triggers', async () => {
|
||||||
@@ -46,7 +46,7 @@ describe('GET /api/v1/apps', () => {
|
|||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
const expectedPayload = getAppsMock(appsWithTriggers);
|
const expectedPayload = getAppsMock(appsWithTriggers);
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return only the apps with actions', async () => {
|
it('should return only the apps with actions', async () => {
|
||||||
@@ -58,6 +58,6 @@ describe('GET /api/v1/apps', () => {
|
|||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
const expectedPayload = getAppsMock(appsWithActions);
|
const expectedPayload = getAppsMock(appsWithActions);
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -29,7 +29,7 @@ describe('GET /api/v1/apps/:appKey/auth-clients/:appAuthClientId', () => {
|
|||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
const expectedPayload = getAppAuthClientMock(currentAppAuthClient);
|
const expectedPayload = getAppAuthClientMock(currentAppAuthClient);
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return not found response for not existing app auth client ID', async () => {
|
it('should return not found response for not existing app auth client ID', async () => {
|
||||||
|
@@ -37,6 +37,6 @@ describe('GET /api/v1/apps/:appKey/auth-clients', () => {
|
|||||||
appAuthClientOne,
|
appAuthClientOne,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -23,7 +23,7 @@ describe('GET /api/v1/apps/:appKey/auth', () => {
|
|||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
const expectedPayload = getAuthMock(exampleApp.auth);
|
const expectedPayload = getAuthMock(exampleApp.auth);
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return not found response for invalid app key', async () => {
|
it('should return not found response for invalid app key', async () => {
|
||||||
|
@@ -32,7 +32,7 @@ describe('GET /api/v1/apps/:appKey/config', () => {
|
|||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
const expectedPayload = getAppConfigMock(appConfig);
|
const expectedPayload = getAppConfigMock(appConfig);
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return not found response for not existing app key', async () => {
|
it('should return not found response for not existing app key', async () => {
|
||||||
|
@@ -47,7 +47,7 @@ describe('GET /api/v1/apps/:appKey/connections', () => {
|
|||||||
currentUserConnectionOne,
|
currentUserConnectionOne,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the connections data of specified app for another user', async () => {
|
it('should return the connections data of specified app for another user', async () => {
|
||||||
@@ -82,19 +82,19 @@ describe('GET /api/v1/apps/:appKey/connections', () => {
|
|||||||
anotherUserConnectionOne,
|
anotherUserConnectionOne,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
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: 'update',
|
action: 'read',
|
||||||
subject: 'Connection',
|
subject: 'Connection',
|
||||||
roleId: currentUserRole.id,
|
roleId: currentUserRole.id,
|
||||||
conditions: ['isCreator'],
|
conditions: ['isCreator'],
|
||||||
});
|
});
|
||||||
|
|
||||||
await request(app)
|
await request(app)
|
||||||
.get('/api/v1/connections/invalid-connection-id/connections')
|
.get('/api/v1/apps/invalid-connection-id/connections')
|
||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.expect(404);
|
.expect(404);
|
||||||
});
|
});
|
||||||
|
@@ -62,7 +62,7 @@ describe('GET /api/v1/apps/:appKey/flows', () => {
|
|||||||
[triggerStepFlowOne, actionStepFlowOne]
|
[triggerStepFlowOne, actionStepFlowOne]
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the flows data of specified app for another user', async () => {
|
it('should return the flows data of specified app for another user', async () => {
|
||||||
@@ -110,7 +110,7 @@ describe('GET /api/v1/apps/:appKey/flows', () => {
|
|||||||
[triggerStepFlowOne, actionStepFlowOne]
|
[triggerStepFlowOne, actionStepFlowOne]
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return not found response for invalid app key', async () => {
|
it('should return not found response for invalid app key', async () => {
|
||||||
|
@@ -29,7 +29,7 @@ describe('GET /api/v1/apps/:appKey/triggers/:triggerKey/substeps', () => {
|
|||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
const expectedPayload = getTriggerSubstepsMock(exampleTrigger.substeps);
|
const expectedPayload = getTriggerSubstepsMock(exampleTrigger.substeps);
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return not found response for invalid app key', async () => {
|
it('should return not found response for invalid app key', async () => {
|
||||||
@@ -47,6 +47,6 @@ describe('GET /api/v1/apps/:appKey/triggers/:triggerKey/substeps', () => {
|
|||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(response.body.data).toEqual([]);
|
expect(response.body.data).toStrictEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -23,7 +23,7 @@ describe('GET /api/v1/apps/:appKey/triggers', () => {
|
|||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
const expectedPayload = getTriggersMock(exampleApp.triggers);
|
const expectedPayload = getTriggersMock(exampleApp.triggers);
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(expectedPayload).toMatchObject(response.body);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return not found response for invalid app key', async () => {
|
it('should return not found response for invalid app key', async () => {
|
||||||
|
@@ -20,6 +20,6 @@ describe('GET /api/v1/automatisch/info', () => {
|
|||||||
|
|
||||||
const expectedPayload = infoMock();
|
const expectedPayload = infoMock();
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -18,6 +18,6 @@ describe('GET /api/v1/automatisch/license', () => {
|
|||||||
|
|
||||||
const expectedPayload = licenseMock();
|
const expectedPayload = licenseMock();
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -10,7 +10,7 @@ describe('GET /api/v1/automatisch/version', () => {
|
|||||||
|
|
||||||
const expectedPayload = {
|
const expectedPayload = {
|
||||||
data: {
|
data: {
|
||||||
version: '0.13.1',
|
version: '0.14.0',
|
||||||
},
|
},
|
||||||
meta: {
|
meta: {
|
||||||
count: 1,
|
count: 1,
|
||||||
@@ -21,6 +21,6 @@ describe('GET /api/v1/automatisch/version', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -69,7 +69,7 @@ describe('GET /api/v1/connections/:connectionId/flows', () => {
|
|||||||
[triggerStepFlowOne, actionStepFlowOne]
|
[triggerStepFlowOne, actionStepFlowOne]
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the flows data of specified connection for another user', async () => {
|
it('should return the flows data of specified connection for another user', async () => {
|
||||||
@@ -123,6 +123,6 @@ describe('GET /api/v1/connections/:connectionId/flows', () => {
|
|||||||
[triggerStepFlowOne, actionStepFlowOne]
|
[triggerStepFlowOne, actionStepFlowOne]
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -43,7 +43,7 @@ describe('POST /api/v1/connections/:connectionId/test', () => {
|
|||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(response.body.data.verified).toEqual(false);
|
expect(response.body.data.verified).toStrictEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update the connection as not verified for another user', async () => {
|
it('should update the connection as not verified for another user', async () => {
|
||||||
@@ -74,7 +74,7 @@ describe('POST /api/v1/connections/:connectionId/test', () => {
|
|||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(response.body.data.verified).toEqual(false);
|
expect(response.body.data.verified).toStrictEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return not found response for not existing connection UUID', async () => {
|
it('should return not found response for not existing connection UUID', async () => {
|
||||||
|
@@ -47,7 +47,7 @@ describe('POST /api/v1/connections/:connectionId/verify', () => {
|
|||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(response.body.data.verified).toEqual(true);
|
expect(response.body.data.verified).toStrictEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return not found response for not existing connection UUID', async () => {
|
it('should return not found response for not existing connection UUID', async () => {
|
||||||
|
@@ -69,7 +69,7 @@ describe('GET /api/v1/executions/:executionId/execution-steps', () => {
|
|||||||
[stepOne, stepTwo]
|
[stepOne, stepTwo]
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the execution steps of another user execution', async () => {
|
it('should return the execution steps of another user execution', async () => {
|
||||||
@@ -118,7 +118,7 @@ describe('GET /api/v1/executions/:executionId/execution-steps', () => {
|
|||||||
[stepOne, stepTwo]
|
[stepOne, stepTwo]
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return not found response for not existing execution step UUID', async () => {
|
it('should return not found response for not existing execution step UUID', async () => {
|
||||||
|
@@ -57,7 +57,7 @@ describe('GET /api/v1/executions/:executionId', () => {
|
|||||||
[stepOne, stepTwo]
|
[stepOne, stepTwo]
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the execution data of another user', async () => {
|
it('should return the execution data of another user', async () => {
|
||||||
@@ -99,7 +99,7 @@ describe('GET /api/v1/executions/:executionId', () => {
|
|||||||
[stepOne, stepTwo]
|
[stepOne, stepTwo]
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return not found response for not existing execution UUID', async () => {
|
it('should return not found response for not existing execution UUID', async () => {
|
||||||
|
@@ -66,7 +66,7 @@ describe('GET /api/v1/executions', () => {
|
|||||||
[stepOne, stepTwo]
|
[stepOne, stepTwo]
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the executions of another user', async () => {
|
it('should return the executions of another user', async () => {
|
||||||
@@ -114,6 +114,6 @@ describe('GET /api/v1/executions', () => {
|
|||||||
[stepOne, stepTwo]
|
[stepOne, stepTwo]
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(response.body).toEqual(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) => {
|
||||||
let flow = await request.currentUser.$relatedQuery('flows').insert({
|
const flow = await request.currentUser.$relatedQuery('flows').insertAndFetch({
|
||||||
name: 'Name your flow',
|
name: 'Name your flow',
|
||||||
});
|
});
|
||||||
|
|
||||||
flow = await flow.createInitialSteps();
|
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.createActionStep(
|
const createdActionStep = await flow.createStepAfter(
|
||||||
request.body.previousStepId
|
request.body.previousStepId
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@@ -41,7 +41,7 @@ describe('GET /api/v1/flows/:flowId', () => {
|
|||||||
actionStep,
|
actionStep,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the flow data of another user', async () => {
|
it('should return the flow data of another user', async () => {
|
||||||
@@ -67,7 +67,7 @@ describe('GET /api/v1/flows/:flowId', () => {
|
|||||||
actionStep,
|
actionStep,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return not found response for not existing flow UUID', async () => {
|
it('should return not found response for not existing flow UUID', async () => {
|
||||||
|
@@ -63,7 +63,7 @@ describe('GET /api/v1/flows', () => {
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the flows data of another user', async () => {
|
it('should return the flows data of another user', async () => {
|
||||||
@@ -113,6 +113,6 @@ describe('GET /api/v1/flows', () => {
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -53,7 +53,7 @@ describe('POST /api/v1/installation/users', () => {
|
|||||||
|
|
||||||
const usersCountAfter = await User.query().resultSize();
|
const usersCountAfter = await User.query().resultSize();
|
||||||
|
|
||||||
expect(usersCountBefore).toEqual(usersCountAfter);
|
expect(usersCountBefore).toStrictEqual(usersCountAfter);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -28,6 +28,6 @@ describe('GET /api/v1/payment/paddle-info', () => {
|
|||||||
|
|
||||||
const expectedResponsePayload = await getPaddleInfoMock();
|
const expectedResponsePayload = await getPaddleInfoMock();
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedResponsePayload);
|
expect(response.body).toStrictEqual(expectedResponsePayload);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -24,6 +24,6 @@ describe('GET /api/v1/payment/plans', () => {
|
|||||||
|
|
||||||
const expectedResponsePayload = await getPaymentPlansMock();
|
const expectedResponsePayload = await getPaymentPlansMock();
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedResponsePayload);
|
expect(response.body).toStrictEqual(expectedResponsePayload);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -25,6 +25,6 @@ describe('GET /api/v1/saml-auth-providers', () => {
|
|||||||
samlAuthProviderOne,
|
samlAuthProviderOne,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -78,7 +78,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-data', () => {
|
|||||||
})
|
})
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(response.body.data).toEqual(repositories);
|
expect(response.body.data).toStrictEqual(repositories);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('of the another users step', async () => {
|
it('of the another users step', async () => {
|
||||||
@@ -117,7 +117,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-data', () => {
|
|||||||
})
|
})
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(response.body.data).toEqual(repositories);
|
expect(response.body.data).toStrictEqual(repositories);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -171,7 +171,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-data', () => {
|
|||||||
})
|
})
|
||||||
.expect(422);
|
.expect(422);
|
||||||
|
|
||||||
expect(response.body.errors).toEqual(errors);
|
expect(response.body.errors).toStrictEqual(errors);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -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)
|
||||||
.get(`/api/v1/steps/${notExistingStepUUID}/dynamic-data`)
|
.post(`/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)
|
||||||
.get(`/api/v1/steps/${step.id}/dynamic-data`)
|
.post(`/api/v1/steps/${step.id}/dynamic-data`)
|
||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.expect(404);
|
.expect(404);
|
||||||
});
|
});
|
||||||
|
@@ -56,7 +56,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-fields', () => {
|
|||||||
|
|
||||||
const expectedPayload = await createDynamicFieldsMock();
|
const expectedPayload = await createDynamicFieldsMock();
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return dynamically created fields of the another users step', async () => {
|
it('should return dynamically created fields of the another users step', async () => {
|
||||||
@@ -97,7 +97,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-fields', () => {
|
|||||||
|
|
||||||
const expectedPayload = await createDynamicFieldsMock();
|
const expectedPayload = await createDynamicFieldsMock();
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return not found response for not existing step UUID', async () => {
|
it('should return not found response for not existing step UUID', async () => {
|
||||||
@@ -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)
|
||||||
.get(`/api/v1/steps/${notExistingStepUUID}/dynamic-fields`)
|
.post(`/api/v1/steps/${notExistingStepUUID}/dynamic-fields`)
|
||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.expect(404);
|
.expect(404);
|
||||||
});
|
});
|
||||||
@@ -138,10 +138,11 @@ describe('POST /api/v1/steps/:stepId/dynamic-fields', () => {
|
|||||||
conditions: [],
|
conditions: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const step = await createStep({ appKey: null });
|
const step = await createStep();
|
||||||
|
await step.$query().patch({ appKey: null });
|
||||||
|
|
||||||
await request(app)
|
await request(app)
|
||||||
.get(`/api/v1/steps/${step.id}/dynamic-fields`)
|
.post(`/api/v1/steps/${step.id}/dynamic-fields`)
|
||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.expect(404);
|
.expect(404);
|
||||||
});
|
});
|
||||||
|
@@ -43,7 +43,7 @@ describe('GET /api/v1/steps/:stepId/connection', () => {
|
|||||||
|
|
||||||
const expectedPayload = await getConnectionMock(currentUserConnection);
|
const expectedPayload = await getConnectionMock(currentUserConnection);
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the current user connection data of specified step', async () => {
|
it('should return the current user connection data of specified step', async () => {
|
||||||
@@ -70,7 +70,7 @@ describe('GET /api/v1/steps/:stepId/connection', () => {
|
|||||||
|
|
||||||
const expectedPayload = await getConnectionMock(anotherUserConnection);
|
const expectedPayload = await getConnectionMock(anotherUserConnection);
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return not found response for not existing step without connection', async () => {
|
it('should return not found response for not existing step without connection', async () => {
|
||||||
|
@@ -70,7 +70,7 @@ describe('GET /api/v1/steps/:stepId/previous-steps', () => {
|
|||||||
[executionStepOne, executionStepTwo]
|
[executionStepOne, executionStepTwo]
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the previous steps of the specified step of another user', async () => {
|
it('should return the previous steps of the specified step of another user', async () => {
|
||||||
@@ -124,7 +124,7 @@ describe('GET /api/v1/steps/:stepId/previous-steps', () => {
|
|||||||
[executionStepOne, executionStepTwo]
|
[executionStepOne, executionStepTwo]
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return not found response for not existing step UUID', async () => {
|
it('should return not found response for not existing step UUID', async () => {
|
||||||
|
@@ -79,7 +79,7 @@ describe('GET /api/v1/users/:userId/apps', () => {
|
|||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
const expectedPayload = getAppsMock();
|
const expectedPayload = getAppsMock();
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return all apps of the another user', async () => {
|
it('should return all apps of the another user', async () => {
|
||||||
@@ -143,7 +143,7 @@ describe('GET /api/v1/users/:userId/apps', () => {
|
|||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
const expectedPayload = getAppsMock();
|
const expectedPayload = getAppsMock();
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return specified app of the current user', async () => {
|
it('should return specified app of the current user', async () => {
|
||||||
@@ -204,7 +204,7 @@ describe('GET /api/v1/users/:userId/apps', () => {
|
|||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(response.body.data.length).toEqual(1);
|
expect(response.body.data.length).toStrictEqual(1);
|
||||||
expect(response.body.data[0].key).toEqual('deepl');
|
expect(response.body.data[0].key).toStrictEqual('deepl');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -39,6 +39,6 @@ describe('GET /api/v1/users/me', () => {
|
|||||||
permissionTwo,
|
permissionTwo,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -29,6 +29,6 @@ describe('GET /api/v1/user/invoices', () => {
|
|||||||
|
|
||||||
const expectedPayload = await getInvoicesMock(invoices);
|
const expectedPayload = await getInvoicesMock(invoices);
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -36,7 +36,7 @@ describe('GET /api/v1/users/:userId/plan-and-usage', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(response.body.data).toEqual(expectedResponseData);
|
expect(response.body.data).toStrictEqual(expectedResponseData);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return current plan and usage data', async () => {
|
it('should return current plan and usage data', async () => {
|
||||||
@@ -63,6 +63,6 @@ describe('GET /api/v1/users/:userId/plan-and-usage', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(response.body.data).toEqual(expectedResponseData);
|
expect(response.body.data).toStrictEqual(expectedResponseData);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -33,7 +33,7 @@ describe('GET /api/v1/users/:userId/subscription', () => {
|
|||||||
|
|
||||||
const expectedPayload = getSubscriptionMock(subscription);
|
const expectedPayload = getSubscriptionMock(subscription);
|
||||||
|
|
||||||
expect(response.body).toEqual(expectedPayload);
|
expect(response.body).toStrictEqual(expectedPayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return not found response if there is no current subscription', async () => {
|
it('should return not found response if there is no current subscription', async () => {
|
||||||
|
@@ -32,7 +32,7 @@ describe('GET /api/v1/users/:userId/trial', () => {
|
|||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
const expectedResponsePayload = await getUserTrialMock(user);
|
const expectedResponsePayload = await getUserTrialMock(user);
|
||||||
expect(response.body).toEqual(expectedResponsePayload);
|
expect(response.body).toStrictEqual(expectedResponsePayload);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -43,7 +43,7 @@ describe('PATCH /api/v1/users/:userId/password', () => {
|
|||||||
.send(userData)
|
.send(userData)
|
||||||
.expect(422);
|
.expect(422);
|
||||||
|
|
||||||
expect(response.body.meta.type).toEqual('ValidationError');
|
expect(response.body.meta.type).toStrictEqual('ValidationError');
|
||||||
expect(response.body.errors).toMatchObject({
|
expect(response.body.errors).toMatchObject({
|
||||||
currentPassword: ['is incorrect.'],
|
currentPassword: ['is incorrect.'],
|
||||||
});
|
});
|
||||||
|
@@ -47,7 +47,8 @@ describe('PATCH /api/v1/users/:userId', () => {
|
|||||||
.send(userData)
|
.send(userData)
|
||||||
.expect(422);
|
.expect(422);
|
||||||
|
|
||||||
expect(response.body.meta.type).toEqual('ModelValidation');
|
expect(response.body.meta.type).toStrictEqual('ModelValidation');
|
||||||
|
|
||||||
expect(response.body.errors).toMatchObject({
|
expect(response.body.errors).toMatchObject({
|
||||||
email: ['must be string'],
|
email: ['must be string'],
|
||||||
fullName: ['must be string'],
|
fullName: ['must be string'],
|
||||||
|
@@ -0,0 +1,52 @@
|
|||||||
|
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');
|
||||||
|
}
|
@@ -30,7 +30,7 @@ const findOrCreateUserBySamlIdentity = async (
|
|||||||
: [mappedUser.role];
|
: [mappedUser.role];
|
||||||
|
|
||||||
const samlAuthProviderRoleMapping = await samlAuthProvider
|
const samlAuthProviderRoleMapping = await samlAuthProvider
|
||||||
.$relatedQuery('samlAuthProvidersRoleMappings')
|
.$relatedQuery('roleMappings')
|
||||||
.whereIn('remote_role_name', mappedRoles)
|
.whereIn('remote_role_name', mappedRoles)
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.first();
|
.first();
|
||||||
|
46
packages/backend/src/helpers/user-ability.test.js
Normal file
46
packages/backend/src/helpers/user-ability.test.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
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([]);
|
||||||
|
});
|
||||||
|
});
|
42
packages/backend/src/models/__snapshots__/flow.test.js.snap
Normal file
42
packages/backend/src/models/__snapshots__/flow.test.js.snap
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
// 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",
|
||||||
|
}
|
||||||
|
`;
|
@@ -0,0 +1,30 @@
|
|||||||
|
// 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",
|
||||||
|
}
|
||||||
|
`;
|
33
packages/backend/src/models/__snapshots__/role.test.js.snap
Normal file
33
packages/backend/src/models/__snapshots__/role.test.js.snap
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`Role model > jsonSchema should have correct validations 1`] = `
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"createdAt": {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"maxLength": 255,
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"minLength": 1,
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"name",
|
||||||
|
],
|
||||||
|
"type": "object",
|
||||||
|
}
|
||||||
|
`;
|
@@ -0,0 +1,72 @@
|
|||||||
|
// 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[`SamlAuthProvidersRoleMapping model > jsonSchema should have the correct schema 1`] = `
|
exports[`RoleMapping model > jsonSchema should have the correct schema 1`] = `
|
||||||
{
|
{
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"id": {
|
||||||
@@ -28,14 +28,3 @@ exports[`SamlAuthProvidersRoleMapping model > jsonSchema should have the correct
|
|||||||
"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],
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
77
packages/backend/src/models/__snapshots__/step.test.js.snap
Normal file
77
packages/backend/src/models/__snapshots__/step.test.js.snap
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
// 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",
|
||||||
|
}
|
||||||
|
`;
|
81
packages/backend/src/models/__snapshots__/user.test.js.snap
Normal file
81
packages/backend/src/models/__snapshots__/user.test.js.snap
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
// 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",
|
||||||
|
}
|
||||||
|
`;
|
@@ -69,7 +69,9 @@ describe('AppAuthClient model', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(formattedAuthDefaults).toStrictEqual(expectedDecryptedValue);
|
expect(formattedAuthDefaults).toStrictEqual(expectedDecryptedValue);
|
||||||
expect(appAuthClient.authDefaults).not.toEqual(formattedAuthDefaults);
|
expect(appAuthClient.authDefaults).not.toStrictEqual(
|
||||||
|
formattedAuthDefaults
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should encrypt formattedAuthDefaults and remove formattedAuthDefaults', async () => {
|
it('should encrypt formattedAuthDefaults and remove formattedAuthDefaults', async () => {
|
||||||
@@ -124,7 +126,9 @@ describe('AppAuthClient model', () => {
|
|||||||
expect(appAuthClient.formattedAuthDefaults).toStrictEqual(
|
expect(appAuthClient.formattedAuthDefaults).toStrictEqual(
|
||||||
formattedAuthDefaults
|
formattedAuthDefaults
|
||||||
);
|
);
|
||||||
expect(appAuthClient.authDefaults).not.toEqual(formattedAuthDefaults);
|
expect(appAuthClient.authDefaults).not.toStrictEqual(
|
||||||
|
formattedAuthDefaults
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -193,7 +193,7 @@ describe('Connection model', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(formattedData).toStrictEqual(expectedDecryptedValue);
|
expect(formattedData).toStrictEqual(expectedDecryptedValue);
|
||||||
expect(connection.data).not.toEqual(formattedData);
|
expect(connection.data).not.toStrictEqual(formattedData);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should encrypt formattedData and remove formattedData', async () => {
|
it('should encrypt formattedData and remove formattedData', async () => {
|
||||||
@@ -243,7 +243,7 @@ describe('Connection model', () => {
|
|||||||
connection.decryptData();
|
connection.decryptData();
|
||||||
|
|
||||||
expect(connection.formattedData).toStrictEqual(formattedData);
|
expect(connection.formattedData).toStrictEqual(formattedData);
|
||||||
expect(connection.data).not.toEqual(formattedData);
|
expect(connection.data).not.toStrictEqual(formattedData);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -88,15 +88,13 @@ class Flow extends Base {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
static async afterFind(args) {
|
static async populateStatusProperty(flows) {
|
||||||
const { result } = args;
|
const referenceFlow = flows[0];
|
||||||
|
|
||||||
const referenceFlow = result[0];
|
|
||||||
|
|
||||||
if (referenceFlow) {
|
if (referenceFlow) {
|
||||||
const shouldBePaused = await referenceFlow.isPaused();
|
const shouldBePaused = await referenceFlow.isPaused();
|
||||||
|
|
||||||
for (const flow of result) {
|
for (const flow of flows) {
|
||||||
if (!flow.active) {
|
if (!flow.active) {
|
||||||
flow.status = 'draft';
|
flow.status = 'draft';
|
||||||
} else if (flow.active && shouldBePaused) {
|
} else if (flow.active && shouldBePaused) {
|
||||||
@@ -108,6 +106,10 @@ 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');
|
||||||
|
|
||||||
@@ -123,13 +125,14 @@ class Flow extends Base {
|
|||||||
return lastExecutions.map((execution) => execution.internalId);
|
return lastExecutions.map((execution) => execution.internalId);
|
||||||
}
|
}
|
||||||
|
|
||||||
get IncompleteStepsError() {
|
static get IncompleteStepsError() {
|
||||||
return new ValidationError({
|
return new ValidationError({
|
||||||
data: {
|
data: {
|
||||||
flow: [
|
flow: [
|
||||||
{
|
{
|
||||||
message: 'All steps should be completed before updating flow status!'
|
message:
|
||||||
}
|
'All steps should be completed before updating flow status!',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
type: 'incompleteStepsError',
|
type: 'incompleteStepsError',
|
||||||
@@ -148,36 +151,48 @@ class Flow extends Base {
|
|||||||
type: 'action',
|
type: 'action',
|
||||||
position: 2,
|
position: 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.$query().withGraphFetched('steps');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async createActionStep(previousStepId) {
|
async getStepById(stepId) {
|
||||||
const previousStep = await this.$relatedQuery('steps')
|
return await this.$relatedQuery('steps').findById(stepId).throwIfNotFound();
|
||||||
.findById(previousStepId)
|
}
|
||||||
.throwIfNotFound();
|
|
||||||
|
|
||||||
const createdStep = await this.$relatedQuery('steps').insertAndFetch({
|
async insertActionStepAtPosition(position) {
|
||||||
|
return await this.$relatedQuery('steps').insertAndFetch({
|
||||||
type: 'action',
|
type: 'action',
|
||||||
position: previousStep.position + 1,
|
position,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const nextSteps = await this.$relatedQuery('steps')
|
async getStepsAfterPosition(position) {
|
||||||
.where('position', '>=', createdStep.position)
|
return await this.$relatedQuery('steps').where('position', '>', position);
|
||||||
.whereNot('id', createdStep.id);
|
}
|
||||||
|
|
||||||
const nextStepQueries = nextSteps.map(async (nextStep, index) => {
|
async updateStepPositionsFrom(startPosition, steps) {
|
||||||
return await nextStep.$query().patchAndFetch({
|
const stepPositionUpdates = steps.map(async (step, index) => {
|
||||||
position: createdStep.position + index + 1,
|
return await step.$query().patch({
|
||||||
|
position: startPosition + index,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
await Promise.all(nextStepQueries);
|
return await Promise.all(stepPositionUpdates);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 delete() {
|
async unregisterWebhook() {
|
||||||
const triggerStep = await this.getTriggerStep();
|
const triggerStep = await this.getTriggerStep();
|
||||||
const trigger = await triggerStep?.getTriggerCommand();
|
const trigger = await triggerStep?.getTriggerCommand();
|
||||||
|
|
||||||
@@ -198,15 +213,33 @@ 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);
|
||||||
|
|
||||||
await ExecutionStep.query().delete().whereIn('execution_id', executionIds);
|
return await ExecutionStep.query()
|
||||||
|
.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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,6 +324,18 @@ 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;
|
||||||
@@ -299,7 +344,7 @@ class Flow extends Base {
|
|||||||
const triggerStep = await this.getTriggerStep();
|
const triggerStep = await this.getTriggerStep();
|
||||||
|
|
||||||
if (triggerStep.status === 'incomplete') {
|
if (triggerStep.status === 'incomplete') {
|
||||||
throw this.IncompleteStepsError;
|
throw Flow.IncompleteStepsError;
|
||||||
}
|
}
|
||||||
|
|
||||||
const trigger = await triggerStep.getTriggerCommand();
|
const trigger = await triggerStep.getTriggerCommand();
|
||||||
@@ -353,60 +398,55 @@ class Flow extends Base {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async $beforeUpdate(opt, queryContext) {
|
async throwIfHavingIncompleteSteps() {
|
||||||
await super.$beforeUpdate(opt, queryContext);
|
const incompleteStep = await this.$relatedQuery('steps').findOne({
|
||||||
|
|
||||||
if (!this.active) return;
|
|
||||||
|
|
||||||
const oldFlow = opt.old;
|
|
||||||
|
|
||||||
const incompleteStep = await oldFlow.$relatedQuery('steps').findOne({
|
|
||||||
status: 'incomplete',
|
status: 'incomplete',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (incompleteStep) {
|
if (incompleteStep) {
|
||||||
throw this.IncompleteStepsError;
|
throw Flow.IncompleteStepsError;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const allSteps = await oldFlow.$relatedQuery('steps');
|
async throwIfHavingLessThanTwoSteps() {
|
||||||
|
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: 'There should be at least one trigger and one action steps in the flow!'
|
message:
|
||||||
}
|
'There should be at least one trigger and one action steps in the flow!',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
type: 'insufficientStepsError',
|
type: 'insufficientStepsError',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
async $beforeUpdate(opt, queryContext) {
|
||||||
|
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;
|
||||||
|
616
packages/backend/src/models/flow.test.js
Normal file
616
packages/backend/src/models/flow.test.js
Normal file
@@ -0,0 +1,616 @@
|
|||||||
|
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({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -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 SamlAuthProvidersRoleMapping extends Base {
|
class RoleMapping extends Base {
|
||||||
static tableName = 'saml_auth_providers_role_mappings';
|
static tableName = 'role_mappings';
|
||||||
|
|
||||||
static jsonSchema = {
|
static jsonSchema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
@@ -21,11 +21,11 @@ class SamlAuthProvidersRoleMapping extends Base {
|
|||||||
relation: Base.BelongsToOneRelation,
|
relation: Base.BelongsToOneRelation,
|
||||||
modelClass: SamlAuthProvider,
|
modelClass: SamlAuthProvider,
|
||||||
join: {
|
join: {
|
||||||
from: 'saml_auth_providers_role_mappings.saml_auth_provider_id',
|
from: 'role_mappings.saml_auth_provider_id',
|
||||||
to: 'saml_auth_providers.id',
|
to: 'saml_auth_providers.id',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SamlAuthProvidersRoleMapping;
|
export default RoleMapping;
|
@@ -1,28 +1,26 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import SamlAuthProvidersRoleMapping from '../models/saml-auth-providers-role-mapping.ee';
|
import RoleMapping from './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('SamlAuthProvidersRoleMapping model', () => {
|
describe('RoleMapping model', () => {
|
||||||
it('tableName should return correct name', () => {
|
it('tableName should return correct name', () => {
|
||||||
expect(SamlAuthProvidersRoleMapping.tableName).toBe(
|
expect(RoleMapping.tableName).toBe('role_mappings');
|
||||||
'saml_auth_providers_role_mappings'
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('jsonSchema should have the correct schema', () => {
|
it('jsonSchema should have the correct schema', () => {
|
||||||
expect(SamlAuthProvidersRoleMapping.jsonSchema).toMatchSnapshot();
|
expect(RoleMapping.jsonSchema).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('relationMappings should return correct associations', () => {
|
it('relationMappings should return correct associations', () => {
|
||||||
const relationMappings = SamlAuthProvidersRoleMapping.relationMappings();
|
const relationMappings = RoleMapping.relationMappings();
|
||||||
|
|
||||||
const expectedRelations = {
|
const expectedRelations = {
|
||||||
samlAuthProvider: {
|
samlAuthProvider: {
|
||||||
relation: Base.BelongsToOneRelation,
|
relation: Base.BelongsToOneRelation,
|
||||||
modelClass: SamlAuthProvider,
|
modelClass: SamlAuthProvider,
|
||||||
join: {
|
join: {
|
||||||
from: 'saml_auth_providers_role_mappings.saml_auth_provider_id',
|
from: 'role_mappings.saml_auth_provider_id',
|
||||||
to: 'saml_auth_providers.id',
|
to: 'saml_auth_providers.id',
|
||||||
},
|
},
|
||||||
},
|
},
|
@@ -52,57 +52,64 @@ class Role extends Base {
|
|||||||
return await this.query().findOne({ name: 'Admin' });
|
return await this.query().findOne({ name: 'Admin' });
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateWithPermissions(data) {
|
async preventAlteringAdmin() {
|
||||||
if (this.isAdmin) {
|
const currentRole = await Role.query().findById(this.id);
|
||||||
|
|
||||||
|
if (currentRole.isAdmin) {
|
||||||
throw new NotAuthorizedError('The admin role cannot be altered!');
|
throw new NotAuthorizedError('The admin role cannot be altered!');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deletePermissions() {
|
||||||
|
return await this.$relatedQuery('permissions').delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
async createPermissions(permissions) {
|
||||||
|
if (permissions?.length) {
|
||||||
|
const validPermissions = Permission.filter(permissions).map(
|
||||||
|
(permission) => ({
|
||||||
|
...permission,
|
||||||
|
roleId: this.id,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await Permission.query().insert(validPermissions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updatePermissions(permissions) {
|
||||||
|
await this.deletePermissions();
|
||||||
|
|
||||||
|
await this.createPermissions(permissions);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateWithPermissions(data) {
|
||||||
const { name, description, permissions } = data;
|
const { name, description, permissions } = data;
|
||||||
|
|
||||||
return await Role.transaction(async (trx) => {
|
await this.updatePermissions(permissions);
|
||||||
await this.$relatedQuery('permissions', trx).delete();
|
|
||||||
|
|
||||||
if (permissions?.length) {
|
await this.$query().patchAndFetch({
|
||||||
const validPermissions = Permission.filter(permissions).map(
|
id: this.id,
|
||||||
(permission) => ({
|
name,
|
||||||
...permission,
|
description,
|
||||||
roleId: this.id,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
await Permission.query().insert(validPermissions);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.$query(trx).patch({
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
});
|
|
||||||
|
|
||||||
return await this.$query(trx)
|
|
||||||
.leftJoinRelated({
|
|
||||||
permissions: true,
|
|
||||||
})
|
|
||||||
.withGraphFetched({
|
|
||||||
permissions: true,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return await this.$query()
|
||||||
|
.leftJoinRelated({
|
||||||
|
permissions: true,
|
||||||
|
})
|
||||||
|
.withGraphFetched({
|
||||||
|
permissions: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteWithPermissions() {
|
async deleteWithPermissions() {
|
||||||
return await Role.transaction(async (trx) => {
|
await this.deletePermissions();
|
||||||
await this.$relatedQuery('permissions', trx).delete();
|
|
||||||
|
|
||||||
return await this.$query(trx).delete();
|
return await this.$query().delete();
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async $beforeDelete(queryContext) {
|
async assertNoRoleUserExists() {
|
||||||
await super.$beforeDelete(queryContext);
|
|
||||||
|
|
||||||
if (this.isAdmin) {
|
|
||||||
throw new NotAuthorizedError('The admin role cannot be deleted!');
|
|
||||||
}
|
|
||||||
|
|
||||||
const userCount = await this.$relatedQuery('users').limit(1).resultSize();
|
const userCount = await this.$relatedQuery('users').limit(1).resultSize();
|
||||||
const hasUsers = userCount > 0;
|
const hasUsers = userCount > 0;
|
||||||
|
|
||||||
@@ -118,7 +125,9 @@ class Role extends Base {
|
|||||||
type: 'ValidationError',
|
type: 'ValidationError',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async assertNoConfigurationUsage() {
|
||||||
const samlAuthProviderUsingDefaultRole = await SamlAuthProvider.query()
|
const samlAuthProviderUsingDefaultRole = await SamlAuthProvider.query()
|
||||||
.where({
|
.where({
|
||||||
default_role_id: this.id,
|
default_role_id: this.id,
|
||||||
@@ -140,6 +149,26 @@ class Role extends Base {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async assertRoleIsNotUsed() {
|
||||||
|
await this.assertNoRoleUserExists();
|
||||||
|
|
||||||
|
await this.assertNoConfigurationUsage();
|
||||||
|
}
|
||||||
|
|
||||||
|
async $beforeUpdate(opt, queryContext) {
|
||||||
|
await super.$beforeUpdate(opt, queryContext);
|
||||||
|
|
||||||
|
await this.preventAlteringAdmin();
|
||||||
|
}
|
||||||
|
|
||||||
|
async $beforeDelete(queryContext) {
|
||||||
|
await super.$beforeDelete(queryContext);
|
||||||
|
|
||||||
|
await this.preventAlteringAdmin();
|
||||||
|
|
||||||
|
await this.assertRoleIsNotUsed();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Role;
|
export default Role;
|
||||||
|
287
packages/backend/src/models/role.test.js
Normal file
287
packages/backend/src/models/role.test.js
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import Role from './role';
|
||||||
|
import Base from './base.js';
|
||||||
|
import Permission from './permission.js';
|
||||||
|
import User from './user.js';
|
||||||
|
import { createRole } from '../../test/factories/role.js';
|
||||||
|
import { createPermission } from '../../test/factories/permission.js';
|
||||||
|
import { createUser } from '../../test/factories/user.js';
|
||||||
|
import { createSamlAuthProvider } from '../../test/factories/saml-auth-provider.ee.js';
|
||||||
|
|
||||||
|
describe('Role model', () => {
|
||||||
|
it('tableName should return correct name', () => {
|
||||||
|
expect(Role.tableName).toBe('roles');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('jsonSchema should have correct validations', () => {
|
||||||
|
expect(Role.jsonSchema).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('relationMappingsshould return correct associations', () => {
|
||||||
|
const relationMappings = Role.relationMappings();
|
||||||
|
|
||||||
|
const expectedRelations = {
|
||||||
|
users: {
|
||||||
|
relation: Base.HasManyRelation,
|
||||||
|
modelClass: User,
|
||||||
|
join: {
|
||||||
|
from: 'roles.id',
|
||||||
|
to: 'users.role_id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
permissions: {
|
||||||
|
relation: Base.HasManyRelation,
|
||||||
|
modelClass: Permission,
|
||||||
|
join: {
|
||||||
|
from: 'roles.id',
|
||||||
|
to: 'permissions.role_id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(relationMappings).toStrictEqual(expectedRelations);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('virtualAttributes should return correct attributes', () => {
|
||||||
|
expect(Role.virtualAttributes).toStrictEqual(['isAdmin']);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isAdmin', () => {
|
||||||
|
it('should return true for admin named role', () => {
|
||||||
|
const role = new Role();
|
||||||
|
role.name = 'Admin';
|
||||||
|
|
||||||
|
expect(role.isAdmin).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for not admin named roles', () => {
|
||||||
|
const role = new Role();
|
||||||
|
role.name = 'User';
|
||||||
|
|
||||||
|
expect(role.isAdmin).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('findAdmin should return admin role', async () => {
|
||||||
|
const createdAdminRole = await createRole({ name: 'Admin' });
|
||||||
|
|
||||||
|
const adminRole = await Role.findAdmin();
|
||||||
|
|
||||||
|
expect(createdAdminRole).toStrictEqual(adminRole);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('preventAlteringAdmin', () => {
|
||||||
|
it('preventAlteringAdmin should throw an error when altering admin role', async () => {
|
||||||
|
const role = await createRole({ name: 'Admin' });
|
||||||
|
|
||||||
|
await expect(() => role.preventAlteringAdmin()).rejects.toThrowError(
|
||||||
|
'The admin role cannot be altered!'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preventAlteringAdmin should not throw an error when altering non-admin roles', async () => {
|
||||||
|
const role = await createRole({ name: 'User' });
|
||||||
|
|
||||||
|
expect(await role.preventAlteringAdmin()).toBe(undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("deletePermissions should delete role's permissions", async () => {
|
||||||
|
const role = await createRole({ name: 'User' });
|
||||||
|
await createPermission({ roleId: role.id });
|
||||||
|
|
||||||
|
await role.deletePermissions();
|
||||||
|
|
||||||
|
expect(await role.$relatedQuery('permissions')).toStrictEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createPermissions', () => {
|
||||||
|
it('should create permissions', async () => {
|
||||||
|
const role = await createRole({ name: 'User' });
|
||||||
|
|
||||||
|
await role.createPermissions([
|
||||||
|
{ action: 'read', subject: 'Flow', conditions: [] },
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(await role.$relatedQuery('permissions')).toMatchObject([
|
||||||
|
{
|
||||||
|
action: 'read',
|
||||||
|
subject: 'Flow',
|
||||||
|
conditions: [],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call Permission.filter', async () => {
|
||||||
|
const role = await createRole({ name: 'User' });
|
||||||
|
|
||||||
|
const permissions = [{ action: 'read', subject: 'Flow', conditions: [] }];
|
||||||
|
|
||||||
|
const permissionFilterSpy = vi
|
||||||
|
.spyOn(Permission, 'filter')
|
||||||
|
.mockReturnValue(permissions);
|
||||||
|
|
||||||
|
await role.createPermissions(permissions);
|
||||||
|
|
||||||
|
expect(permissionFilterSpy).toHaveBeenCalledWith(permissions);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updatePermissions should delete existing permissions and create new permissions', async () => {
|
||||||
|
const permissionsData = [
|
||||||
|
{ action: 'read', subject: 'Flow', conditions: [] },
|
||||||
|
];
|
||||||
|
|
||||||
|
const deletePermissionsSpy = vi
|
||||||
|
.spyOn(Role.prototype, 'deletePermissions')
|
||||||
|
.mockResolvedValueOnce();
|
||||||
|
const createPermissionsSpy = vi
|
||||||
|
.spyOn(Role.prototype, 'createPermissions')
|
||||||
|
.mockResolvedValueOnce();
|
||||||
|
|
||||||
|
const role = await createRole({ name: 'User' });
|
||||||
|
|
||||||
|
await role.updatePermissions(permissionsData);
|
||||||
|
|
||||||
|
expect(deletePermissionsSpy.mock.invocationCallOrder[0]).toBeLessThan(
|
||||||
|
createPermissionsSpy.mock.invocationCallOrder[0]
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(deletePermissionsSpy).toHaveBeenNthCalledWith(1);
|
||||||
|
expect(createPermissionsSpy).toHaveBeenNthCalledWith(1, permissionsData);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateWithPermissions', () => {
|
||||||
|
it('should update role along with given permissions', async () => {
|
||||||
|
const role = await createRole({ name: 'User' });
|
||||||
|
await createPermission({
|
||||||
|
roleId: role.id,
|
||||||
|
subject: 'Flow',
|
||||||
|
action: 'read',
|
||||||
|
conditions: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const newRoleData = {
|
||||||
|
name: 'Updated user',
|
||||||
|
description: 'Updated description',
|
||||||
|
permissions: [
|
||||||
|
{
|
||||||
|
action: 'update',
|
||||||
|
subject: 'Flow',
|
||||||
|
conditions: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
await role.updateWithPermissions(newRoleData);
|
||||||
|
|
||||||
|
const roleWithPermissions = await role
|
||||||
|
.$query()
|
||||||
|
.leftJoinRelated({ permissions: true })
|
||||||
|
.withGraphFetched({ permissions: true });
|
||||||
|
|
||||||
|
expect(roleWithPermissions).toMatchObject(newRoleData);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteWithPermissions', () => {
|
||||||
|
it('should delete role along with given permissions', async () => {
|
||||||
|
const role = await createRole({ name: 'User' });
|
||||||
|
await createPermission({
|
||||||
|
roleId: role.id,
|
||||||
|
subject: 'Flow',
|
||||||
|
action: 'read',
|
||||||
|
conditions: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
await role.deleteWithPermissions();
|
||||||
|
|
||||||
|
const refetchedRole = await role.$query();
|
||||||
|
const rolePermissions = await Permission.query().where({
|
||||||
|
roleId: role.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(refetchedRole).toBe(undefined);
|
||||||
|
expect(rolePermissions).toStrictEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('assertNoRoleUserExists', () => {
|
||||||
|
it('should reject with an error when the role has users', async () => {
|
||||||
|
const role = await createRole({ name: 'User' });
|
||||||
|
await createUser({ roleId: role.id });
|
||||||
|
|
||||||
|
await expect(() => role.assertNoRoleUserExists()).rejects.toThrowError(
|
||||||
|
`All users must be migrated away from the "User" role.`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve when the role does not have any users', async () => {
|
||||||
|
const role = await createRole();
|
||||||
|
|
||||||
|
expect(await role.assertNoRoleUserExists()).toBe(undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('assertNoConfigurationUsage', () => {
|
||||||
|
it('should reject with an error when the role is used in configuration', async () => {
|
||||||
|
const role = await createRole();
|
||||||
|
await createSamlAuthProvider({ defaultRoleId: role.id });
|
||||||
|
|
||||||
|
await expect(() =>
|
||||||
|
role.assertNoConfigurationUsage()
|
||||||
|
).rejects.toThrowError(
|
||||||
|
'samlAuthProvider: You need to change the default role in the SAML configuration before deleting this role.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve when the role does not have any users', async () => {
|
||||||
|
const role = await createRole();
|
||||||
|
|
||||||
|
expect(await role.assertNoConfigurationUsage()).toBe(undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('assertRoleIsNotUsed should call assertNoRoleUserExists and assertNoConfigurationUsage', async () => {
|
||||||
|
const role = new Role();
|
||||||
|
|
||||||
|
const assertNoRoleUserExistsSpy = vi
|
||||||
|
.spyOn(role, 'assertNoRoleUserExists')
|
||||||
|
.mockResolvedValue();
|
||||||
|
|
||||||
|
const assertNoConfigurationUsageSpy = vi
|
||||||
|
.spyOn(role, 'assertNoConfigurationUsage')
|
||||||
|
.mockResolvedValue();
|
||||||
|
|
||||||
|
await role.assertRoleIsNotUsed();
|
||||||
|
|
||||||
|
expect(assertNoRoleUserExistsSpy).toHaveBeenCalledOnce();
|
||||||
|
expect(assertNoConfigurationUsageSpy).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('$beforeDelete', () => {
|
||||||
|
it('should call preventAlteringAdmin', async () => {
|
||||||
|
const role = await createRole({ name: 'User' });
|
||||||
|
|
||||||
|
const preventAlteringAdminSpy = vi
|
||||||
|
.spyOn(role, 'preventAlteringAdmin')
|
||||||
|
.mockResolvedValue();
|
||||||
|
|
||||||
|
await role.$query().delete();
|
||||||
|
|
||||||
|
expect(preventAlteringAdminSpy).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call assertRoleIsNotUsed', async () => {
|
||||||
|
const role = await createRole({ name: 'User' });
|
||||||
|
|
||||||
|
const assertRoleIsNotUsedSpy = vi
|
||||||
|
.spyOn(role, 'assertRoleIsNotUsed')
|
||||||
|
.mockResolvedValue();
|
||||||
|
|
||||||
|
await role.$query().delete();
|
||||||
|
|
||||||
|
expect(assertRoleIsNotUsedSpy).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -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 SamlAuthProvidersRoleMapping from './saml-auth-providers-role-mapping.ee.js';
|
import RoleMapping from './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',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
samlAuthProvidersRoleMappings: {
|
roleMappings: {
|
||||||
relation: Base.HasManyRelation,
|
relation: Base.HasManyRelation,
|
||||||
modelClass: SamlAuthProvidersRoleMapping,
|
modelClass: RoleMapping,
|
||||||
join: {
|
join: {
|
||||||
from: 'saml_auth_providers.id',
|
from: 'saml_auth_providers.id',
|
||||||
to: 'saml_auth_providers_role_mappings.saml_auth_provider_id',
|
to: 'role_mappings.saml_auth_provider_id',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -133,27 +133,22 @@ class SamlAuthProvider extends Base {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async updateRoleMappings(roleMappings) {
|
async updateRoleMappings(roleMappings) {
|
||||||
return await SamlAuthProvider.transaction(async (trx) => {
|
await this.$relatedQuery('roleMappings').delete();
|
||||||
await this.$relatedQuery('samlAuthProvidersRoleMappings', trx).delete();
|
|
||||||
|
|
||||||
if (isEmpty(roleMappings)) {
|
if (isEmpty(roleMappings)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const samlAuthProvidersRoleMappingsData = roleMappings.map(
|
const roleMappingsData = roleMappings.map((roleMapping) => ({
|
||||||
(samlAuthProvidersRoleMapping) => ({
|
...roleMapping,
|
||||||
...samlAuthProvidersRoleMapping,
|
samlAuthProviderId: this.id,
|
||||||
samlAuthProviderId: this.id,
|
}));
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const samlAuthProvidersRoleMappings =
|
const newRoleMappings = await RoleMapping.query().insertAndFetch(
|
||||||
await SamlAuthProvidersRoleMapping.query(trx).insertAndFetch(
|
roleMappingsData
|
||||||
samlAuthProvidersRoleMappingsData
|
);
|
||||||
);
|
|
||||||
|
|
||||||
return samlAuthProvidersRoleMappings;
|
return newRoleMappings;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
231
packages/backend/src/models/saml-auth-provider.ee.test.js
Normal file
231
packages/backend/src/models/saml-auth-provider.ee.test.js
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -93,6 +93,14 @@ 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;
|
||||||
|
|
||||||
@@ -135,24 +143,6 @@ 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;
|
||||||
|
|
||||||
@@ -170,12 +160,7 @@ class Step extends Base {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getLastExecutionStep() {
|
async getLastExecutionStep() {
|
||||||
const lastExecutionStep = await this.$relatedQuery('executionSteps')
|
return await this.$relatedQuery('lastExecutionStep');
|
||||||
.orderBy('created_at', 'desc')
|
|
||||||
.limit(1)
|
|
||||||
.first();
|
|
||||||
|
|
||||||
return lastExecutionStep;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getNextStep() {
|
async getNextStep() {
|
||||||
@@ -207,19 +192,18 @@ class Step extends Base {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getSetupFields() {
|
async getSetupFields() {
|
||||||
let setupSupsteps;
|
let substeps;
|
||||||
|
|
||||||
if (this.isTrigger) {
|
if (this.isTrigger) {
|
||||||
setupSupsteps = (await this.getTriggerCommand()).substeps;
|
substeps = (await this.getTriggerCommand()).substeps;
|
||||||
} else {
|
} else {
|
||||||
setupSupsteps = (await this.getActionCommand()).substeps;
|
substeps = (await this.getActionCommand()).substeps;
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingArguments = setupSupsteps.find(
|
const setupSubstep = substeps.find(
|
||||||
(substep) => substep.key === 'chooseTrigger'
|
(substep) => substep.key === 'chooseTrigger'
|
||||||
).arguments;
|
);
|
||||||
|
return setupSubstep.arguments;
|
||||||
return existingArguments;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSetupAndDynamicFields() {
|
async getSetupAndDynamicFields() {
|
||||||
@@ -326,23 +310,17 @@ class Step extends Base {
|
|||||||
.$relatedQuery('steps')
|
.$relatedQuery('steps')
|
||||||
.where('position', '>', this.position);
|
.where('position', '>', this.position);
|
||||||
|
|
||||||
const nextStepQueries = nextSteps.map(async (nextStep) => {
|
await flow.updateStepPositionsFrom(this.position, nextSteps);
|
||||||
await nextStep.$query().patch({
|
|
||||||
position: nextStep.position - 1,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.all(nextStepQueries);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateFor(user, newStepData) {
|
async updateFor(user, newStepData) {
|
||||||
const { connectionId, appKey, key, parameters } = newStepData;
|
const { appKey = this.appKey, connectionId, key, parameters } = newStepData;
|
||||||
|
|
||||||
if (connectionId && (appKey || this.appKey)) {
|
if (connectionId && appKey) {
|
||||||
await user.authorizedConnections
|
await user.authorizedConnections
|
||||||
.findOne({
|
.findOne({
|
||||||
id: connectionId,
|
id: connectionId,
|
||||||
key: appKey || this.appKey,
|
key: appKey,
|
||||||
})
|
})
|
||||||
.throwIfNotFound();
|
.throwIfNotFound();
|
||||||
}
|
}
|
||||||
@@ -356,8 +334,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',
|
||||||
@@ -367,6 +345,16 @@ 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;
|
||||||
|
504
packages/backend/src/models/step.test.js
Normal file
504
packages/backend/src/models/step.test.js
Normal file
@@ -0,0 +1,504 @@
|
|||||||
|
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,6 +212,10 @@ 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,
|
||||||
@@ -223,8 +227,8 @@ class User extends Base {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
login(password) {
|
async login(password) {
|
||||||
return bcrypt.compare(password, this.password);
|
return await bcrypt.compare(password, this.password);
|
||||||
}
|
}
|
||||||
|
|
||||||
async generateResetPasswordToken() {
|
async generateResetPasswordToken() {
|
||||||
@@ -407,7 +411,7 @@ class User extends Base {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async startTrialPeriod() {
|
startTrialPeriod() {
|
||||||
this.trialExpiryDate = DateTime.now().plus({ days: 30 }).toISODate();
|
this.trialExpiryDate = DateTime.now().plus({ days: 30 }).toISODate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -583,32 +587,30 @@ class User extends Base {
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
async $beforeInsert(queryContext) {
|
can(action, subject) {
|
||||||
await super.$beforeInsert(queryContext);
|
const can = this.ability.can(action, subject);
|
||||||
|
|
||||||
this.email = this.email.toLowerCase();
|
if (!can) throw new NotAuthorizedError('The user is not authorized!');
|
||||||
await this.generateHash();
|
|
||||||
|
|
||||||
if (appConfig.isCloud) {
|
const relevantRule = this.ability.relevantRuleFor(action, subject);
|
||||||
await this.startTrialPeriod();
|
|
||||||
}
|
const conditions = relevantRule?.conditions || [];
|
||||||
|
const conditionMap = Object.fromEntries(
|
||||||
|
conditions.map((condition) => [condition, true])
|
||||||
|
);
|
||||||
|
|
||||||
|
return conditionMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
async $beforeUpdate(opt, queryContext) {
|
lowercaseEmail() {
|
||||||
await super.$beforeUpdate(opt, queryContext);
|
|
||||||
|
|
||||||
if (this.email) {
|
if (this.email) {
|
||||||
this.email = this.email.toLowerCase();
|
this.email = this.email.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.generateHash();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async $afterInsert(queryContext) {
|
async createUsageData() {
|
||||||
await super.$afterInsert(queryContext);
|
|
||||||
|
|
||||||
if (appConfig.isCloud) {
|
if (appConfig.isCloud) {
|
||||||
await this.$relatedQuery('usageData').insert({
|
return await this.$relatedQuery('usageData').insertAndFetch({
|
||||||
userId: this.id,
|
userId: this.id,
|
||||||
consumedTaskCount: 0,
|
consumedTaskCount: 0,
|
||||||
nextResetAt: DateTime.now().plus({ days: 30 }).toISODate(),
|
nextResetAt: DateTime.now().plus({ days: 30 }).toISODate(),
|
||||||
@@ -616,8 +618,10 @@ class User extends Base {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async $afterFind() {
|
async omitEnterprisePermissionsWithoutValidLicense() {
|
||||||
if (await hasValidLicense()) return this;
|
if (await hasValidLicense()) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
if (Array.isArray(this.permissions)) {
|
if (Array.isArray(this.permissions)) {
|
||||||
this.permissions = this.permissions.filter((permission) => {
|
this.permissions = this.permissions.filter((permission) => {
|
||||||
@@ -631,35 +635,35 @@ class User extends Base {
|
|||||||
return !restrictedSubjects.includes(permission.subject);
|
return !restrictedSubjects.includes(permission.subject);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get ability() {
|
async $beforeInsert(queryContext) {
|
||||||
return userAbility(this);
|
await super.$beforeInsert(queryContext);
|
||||||
|
|
||||||
|
this.lowercaseEmail();
|
||||||
|
await this.generateHash();
|
||||||
|
|
||||||
|
if (appConfig.isCloud) {
|
||||||
|
this.startTrialPeriod();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
can(action, subject) {
|
async $beforeUpdate(opt, queryContext) {
|
||||||
const can = this.ability.can(action, subject);
|
await super.$beforeUpdate(opt, queryContext);
|
||||||
|
|
||||||
if (!can) throw new NotAuthorizedError();
|
this.lowercaseEmail();
|
||||||
|
|
||||||
const relevantRule = this.ability.relevantRuleFor(action, subject);
|
await this.generateHash();
|
||||||
|
|
||||||
const conditions = relevantRule?.conditions || [];
|
|
||||||
const conditionMap = Object.fromEntries(
|
|
||||||
conditions.map((condition) => [condition, true])
|
|
||||||
);
|
|
||||||
|
|
||||||
return conditionMap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cannot(action, subject) {
|
async $afterInsert(queryContext) {
|
||||||
const cannot = this.ability.cannot(action, subject);
|
await super.$afterInsert(queryContext);
|
||||||
|
|
||||||
if (cannot) throw new NotAuthorizedError();
|
await this.createUsageData();
|
||||||
|
}
|
||||||
|
|
||||||
return cannot;
|
async $afterFind() {
|
||||||
|
await this.omitEnterprisePermissionsWithoutValidLicense();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
1529
packages/backend/src/models/user.test.js
Normal file
1529
packages/backend/src/models/user.test.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -11,10 +11,6 @@ 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,10 +11,6 @@ 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,10 +11,6 @@ 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(
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user