Compare commits
169 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
bab87e9e67 | ||
![]() |
7d793ce2a2 | ||
![]() |
f83deac469 | ||
![]() |
b8da721e39 | ||
![]() |
db8b98ca16 | ||
![]() |
01b8c600fe | ||
![]() |
69bd5549a2 | ||
![]() |
bc631e3931 | ||
![]() |
8ca4bc5a33 | ||
![]() |
58a569afb0 | ||
![]() |
db718d6fc3 | ||
![]() |
ca9cb8b07b | ||
![]() |
ef14586412 | ||
![]() |
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 | ||
![]() |
79050af391 |
@@ -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,23 +24,34 @@ WEBHOOK_SECRET_KEY=sample_webhook_secret_key
|
|||||||
APP_SECRET_KEY=sample_app_secret_key
|
APP_SECRET_KEY=sample_app_secret_key
|
||||||
REDIS_HOST=redis
|
REDIS_HOST=redis
|
||||||
SERVE_WEB_APP_SEPARATELY=true" >> .env
|
SERVE_WEB_APP_SEPARATELY=true" >> .env
|
||||||
|
|
||||||
|
echo "Installing backend dependencies..."
|
||||||
|
|
||||||
|
yarn
|
||||||
|
|
||||||
cd $CURRENT_DIR
|
cd $CURRENT_DIR
|
||||||
|
|
||||||
echo "Configuring web environment variables..."
|
echo "Configuring web environment variables..."
|
||||||
|
|
||||||
cd packages/web
|
cd packages/web
|
||||||
|
|
||||||
rm -rf .env
|
rm -rf .env
|
||||||
|
|
||||||
echo "
|
echo "
|
||||||
PORT=$WEB_PORT
|
PORT=$WEB_PORT
|
||||||
REACT_APP_BACKEND_URL=http://localhost:$BACKEND_PORT
|
REACT_APP_BACKEND_URL=http://localhost:$BACKEND_PORT
|
||||||
" >> .env
|
" >> .env
|
||||||
|
|
||||||
|
echo "Installing web dependencies..."
|
||||||
|
|
||||||
|
yarn
|
||||||
|
|
||||||
cd $CURRENT_DIR
|
cd $CURRENT_DIR
|
||||||
|
|
||||||
echo "Installing and linking dependencies..."
|
|
||||||
yarn
|
|
||||||
yarn lerna bootstrap
|
|
||||||
|
|
||||||
echo "Migrating database..."
|
echo "Migrating database..."
|
||||||
|
|
||||||
cd packages/backend
|
cd packages/backend
|
||||||
|
|
||||||
yarn db:migrate
|
yarn db:migrate
|
||||||
yarn db:seed:user
|
yarn db:seed:user
|
||||||
|
|
||||||
|
9
.github/workflows/backend.yml
vendored
9
.github/workflows/backend.yml
vendored
@@ -41,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
|
||||||
|
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 }}."
|
||||||
|
36
.github/workflows/playwright.yml
vendored
36
.github/workflows/playwright.yml
vendored
@@ -55,19 +55,44 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: '18'
|
||||||
- name: Install dependencies
|
cache: 'yarn'
|
||||||
run: yarn && yarn lerna bootstrap
|
cache-dependency-path: |
|
||||||
|
packages/backend/yarn.lock
|
||||||
|
packages/web/yarn.lock
|
||||||
|
packages/e2e-tests/yarn.lock
|
||||||
|
- name: Install backend dependencies
|
||||||
|
run: yarn --frozen-lockfile
|
||||||
|
working-directory: ./packages/backend
|
||||||
|
- name: Install web dependencies
|
||||||
|
run: yarn --frozen-lockfile
|
||||||
|
working-directory: ./packages/web
|
||||||
|
- name: Install e2e-tests dependencies
|
||||||
|
run: yarn --frozen-lockfile
|
||||||
|
working-directory: ./packages/e2e-tests
|
||||||
|
- name: Get installed Playwright version
|
||||||
|
id: playwright-version
|
||||||
|
run: echo "PLAYWRIGHT_VERSION=$(node -e "console.log(require('./package.json').devDependencies['@playwright/test'])")" >> $GITHUB_ENV
|
||||||
|
working-directory: ./packages/e2e-tests
|
||||||
|
- name: Cache playwright binaries
|
||||||
|
uses: actions/cache@v3
|
||||||
|
id: playwright-cache
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache/ms-playwright
|
||||||
|
key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }}
|
||||||
- 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
|
||||||
|
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||||
- name: Build Automatisch web
|
- name: Build Automatisch web
|
||||||
working-directory: ./packages/web
|
|
||||||
run: yarn build
|
run: yarn build
|
||||||
env:
|
env:
|
||||||
# Keep this until we clean up warnings in build processes
|
# Keep this until we clean up warnings in build processes
|
||||||
CI: false
|
CI: false
|
||||||
|
working-directory: ./packages/web
|
||||||
- name: Migrate database
|
- name: Migrate database
|
||||||
working-directory: ./packages/backend
|
working-directory: ./packages/backend
|
||||||
run: yarn db:migrate
|
run: yarn db:migrate
|
||||||
@@ -107,6 +132,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
LOGIN_EMAIL: user@automatisch.io
|
LOGIN_EMAIL: user@automatisch.io
|
||||||
LOGIN_PASSWORD: sample
|
LOGIN_PASSWORD: sample
|
||||||
|
BACKEND_APP_URL: http://localhost:3000
|
||||||
BASE_URL: http://localhost:3000
|
BASE_URL: http://localhost:3000
|
||||||
GITHUB_CLIENT_ID: 1c0417daf898adfbd99a
|
GITHUB_CLIENT_ID: 1c0417daf898adfbd99a
|
||||||
GITHUB_CLIENT_SECRET: 3328fa814dd582ccd03dbe785cfd683fb8da92b3
|
GITHUB_CLIENT_SECRET: 3328fa814dd582ccd03dbe785cfd683fb8da92b3
|
||||||
|
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"
|
|
||||||
}
|
|
||||||
}
|
|
@@ -23,6 +23,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 +37,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 +65,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",
|
||||||
|
@@ -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'),
|
||||||
|
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@@ -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,
|
||||||
|
@@ -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
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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",
|
||||||
|
}
|
||||||
|
`;
|
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",
|
||||||
|
}
|
||||||
|
`;
|
@@ -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",
|
||||||
|
}
|
||||||
|
`;
|
@@ -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({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
84
packages/backend/src/models/saml-auth-provider.ee.test.js
Normal file
84
packages/backend/src/models/saml-auth-provider.ee.test.js
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import { vi, describe, it, expect } from 'vitest';
|
||||||
|
import SamlAuthProvider from '../models/saml-auth-provider.ee';
|
||||||
|
import SamlAuthProvidersRoleMapping from '../models/saml-auth-providers-role-mapping.ee';
|
||||||
|
import Identity from './identity.ee';
|
||||||
|
import Base from './base';
|
||||||
|
import appConfig from '../config/app';
|
||||||
|
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
samlAuthProvidersRoleMappings: {
|
||||||
|
relation: Base.HasManyRelation,
|
||||||
|
modelClass: SamlAuthProvidersRoleMapping,
|
||||||
|
join: {
|
||||||
|
from: 'saml_auth_providers.id',
|
||||||
|
to: 'saml_auth_providers_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'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
@@ -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({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -223,8 +223,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 +407,7 @@ class User extends Base {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async startTrialPeriod() {
|
startTrialPeriod() {
|
||||||
this.trialExpiryDate = DateTime.now().plus({ days: 30 }).toISODate();
|
this.trialExpiryDate = DateTime.now().plus({ days: 30 }).toISODate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -590,7 +590,7 @@ class User extends Base {
|
|||||||
await this.generateHash();
|
await this.generateHash();
|
||||||
|
|
||||||
if (appConfig.isCloud) {
|
if (appConfig.isCloud) {
|
||||||
await this.startTrialPeriod();
|
this.startTrialPeriod();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -642,7 +642,7 @@ class User extends Base {
|
|||||||
can(action, subject) {
|
can(action, subject) {
|
||||||
const can = this.ability.can(action, subject);
|
const can = this.ability.can(action, subject);
|
||||||
|
|
||||||
if (!can) throw new NotAuthorizedError();
|
if (!can) throw new NotAuthorizedError('The user is not authorized!');
|
||||||
|
|
||||||
const relevantRule = this.ability.relevantRuleFor(action, subject);
|
const relevantRule = this.ability.relevantRuleFor(action, subject);
|
||||||
|
|
||||||
|
1187
packages/backend/src/models/user.test.js
Normal file
1187
packages/backend/src/models/user.test.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
import Role from '../../src/models/role';
|
import Role from '../../src/models/role.js';
|
||||||
|
|
||||||
export const createRole = async (params = {}) => {
|
export const createRole = async (params = {}) => {
|
||||||
const name = faker.lorem.word();
|
const name = faker.lorem.word();
|
||||||
|
@@ -6,18 +6,6 @@ const createFlowMock = async (flow) => {
|
|||||||
status: flow.status,
|
status: flow.status,
|
||||||
createdAt: flow.createdAt.getTime(),
|
createdAt: flow.createdAt.getTime(),
|
||||||
updatedAt: flow.updatedAt.getTime(),
|
updatedAt: flow.updatedAt.getTime(),
|
||||||
steps: [
|
|
||||||
{
|
|
||||||
position: 1,
|
|
||||||
status: 'incomplete',
|
|
||||||
type: 'trigger',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
position: 2,
|
|
||||||
status: 'incomplete',
|
|
||||||
type: 'action',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
4807
packages/backend/yarn.lock
Normal file
4807
packages/backend/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
1
packages/docs/.gitignore
vendored
Normal file
1
packages/docs/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pages/.vitepress/cache
|
@@ -4,6 +4,7 @@
|
|||||||
"license": "See LICENSE file",
|
"license": "See LICENSE file",
|
||||||
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
|
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vitepress dev pages --port 3002",
|
"dev": "vitepress dev pages --port 3002",
|
||||||
"build": "vitepress build pages",
|
"build": "vitepress build pages",
|
||||||
|
@@ -6,11 +6,19 @@ Clone main branch of Automatisch.
|
|||||||
git clone git@github.com:automatisch/automatisch.git
|
git clone git@github.com:automatisch/automatisch.git
|
||||||
```
|
```
|
||||||
|
|
||||||
Then, install the dependencies.
|
Then, install the dependencies for both backend and web packages separately.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd automatisch
|
cd automatisch
|
||||||
|
|
||||||
|
# Install backend dependencies
|
||||||
|
cd packages/backend
|
||||||
yarn install
|
yarn install
|
||||||
|
|
||||||
|
# Install web dependencies
|
||||||
|
cd packages/web
|
||||||
|
yarn install
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Backend
|
## Backend
|
||||||
@@ -53,12 +61,14 @@ yarn db:seed:user
|
|||||||
Start the main backend server.
|
Start the main backend server.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
cd packages/backend
|
||||||
yarn dev
|
yarn dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Start the worker server in another terminal tab.
|
Start the worker server in another terminal tab.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
cd packages/backend
|
||||||
yarn worker
|
yarn worker
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -84,6 +94,7 @@ It will automatically open [http://localhost:3001](http://localhost:3001) in you
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd packages/docs
|
cd packages/docs
|
||||||
|
yarn install
|
||||||
yarn dev
|
yarn dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
# Repository Structure
|
# Repository Structure
|
||||||
|
|
||||||
We use `lerna` with `yarn workspaces` to manage the mono repository. We have the following packages:
|
We manage a monorepo structure with the following packages:
|
||||||
|
|
||||||
```
|
```
|
||||||
.
|
.
|
||||||
@@ -15,3 +15,5 @@ We use `lerna` with `yarn workspaces` to manage the mono repository. We have the
|
|||||||
- `docs` - The docs package contains the documentation website.
|
- `docs` - The docs package contains the documentation website.
|
||||||
- `e2e-tests` - The e2e-tests package contains the end-to-end tests for the internal usage.
|
- `e2e-tests` - The e2e-tests package contains the end-to-end tests for the internal usage.
|
||||||
- `web` - The web package contains the frontend application of Automatisch.
|
- `web` - The web package contains the frontend application of Automatisch.
|
||||||
|
|
||||||
|
Each package is independently managed, and has its own package.json file to manage dependencies. This allows for better isolation and flexibility.
|
||||||
|
1192
packages/docs/yarn.lock
Normal file
1192
packages/docs/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3,3 +3,4 @@ POSTGRES_USER=automatisch_user
|
|||||||
POSTGRES_PASSWORD=automatisch_password
|
POSTGRES_PASSWORD=automatisch_password
|
||||||
POSTGRES_PORT=5432
|
POSTGRES_PORT=5432
|
||||||
POSTGRES_HOST=localhost
|
POSTGRES_HOST=localhost
|
||||||
|
BACKEND_APP_URL=http://localhost:3000
|
@@ -20,44 +20,54 @@ export class AdminApplicationSettingsPage extends AuthenticatedPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async allowCustomConnections() {
|
async allowCustomConnections() {
|
||||||
await expect(this.disableConnectionsSwitch).not.toBeChecked();
|
await expect(this.allowCustomConnectionsSwitch).not.toBeChecked();
|
||||||
await this.allowCustomConnectionsSwitch.check();
|
await this.allowCustomConnectionsSwitch.check();
|
||||||
|
await expect(this.allowCustomConnectionsSwitch).toBeChecked();
|
||||||
await this.saveButton.click();
|
await this.saveButton.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
async allowSharedConnections() {
|
async allowSharedConnections() {
|
||||||
await expect(this.disableConnectionsSwitch).not.toBeChecked();
|
await expect(this.allowSharedConnectionsSwitch).not.toBeChecked();
|
||||||
await this.allowSharedConnectionsSwitch.check();
|
await this.allowSharedConnectionsSwitch.check();
|
||||||
|
await expect(this.allowSharedConnectionsSwitch).toBeChecked();
|
||||||
await this.saveButton.click();
|
await this.saveButton.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
async disallowConnections() {
|
async disallowConnections() {
|
||||||
await expect(this.disableConnectionsSwitch).not.toBeChecked();
|
await expect(this.disableConnectionsSwitch).not.toBeChecked();
|
||||||
await this.disableConnectionsSwitch.check();
|
await this.disableConnectionsSwitch.check();
|
||||||
|
await expect(this.disableConnectionsSwitch).toBeChecked();
|
||||||
await this.saveButton.click();
|
await this.saveButton.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
async disallowCustomConnections() {
|
async disallowCustomConnections() {
|
||||||
await expect(this.disableConnectionsSwitch).toBeChecked();
|
await expect(this.allowCustomConnectionsSwitch).toBeChecked();
|
||||||
await this.allowCustomConnectionsSwitch.uncheck();
|
await this.allowCustomConnectionsSwitch.uncheck();
|
||||||
|
await expect(this.allowCustomConnectionsSwitch).not.toBeChecked();
|
||||||
await this.saveButton.click();
|
await this.saveButton.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
async disallowSharedConnections() {
|
async disallowSharedConnections() {
|
||||||
await expect(this.disableConnectionsSwitch).toBeChecked();
|
await expect(this.allowSharedConnectionsSwitch).toBeChecked();
|
||||||
await this.allowSharedConnectionsSwitch.uncheck();
|
await this.allowSharedConnectionsSwitch.uncheck();
|
||||||
|
await expect(this.allowSharedConnectionsSwitch).not.toBeChecked();
|
||||||
await this.saveButton.click();
|
await this.saveButton.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
async allowConnections() {
|
async allowConnections() {
|
||||||
await expect(this.disableConnectionsSwitch).toBeChecked();
|
await expect(this.disableConnectionsSwitch).toBeChecked();
|
||||||
await this.disableConnectionsSwitch.uncheck();
|
await this.disableConnectionsSwitch.uncheck();
|
||||||
|
await expect(this.disableConnectionsSwitch).not.toBeChecked();
|
||||||
await this.saveButton.click();
|
await this.saveButton.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
async expectSuccessSnackbarToBeVisible() {
|
async expectSuccessSnackbarToBeVisible() {
|
||||||
await expect(this.successSnackbar).toHaveCount(1);
|
const snackbars = await this.successSnackbar.all();
|
||||||
await this.successSnackbar.click();
|
for (const snackbar of snackbars) {
|
||||||
await expect(this.successSnackbar).toHaveCount(0);
|
await expect(await snackbar.getAttribute('data-snackbar-variant')).toBe(
|
||||||
|
'success'
|
||||||
|
);
|
||||||
|
// await snackbar.click();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
import { expect } from '@playwright/test';
|
||||||
|
|
||||||
const { AuthenticatedPage } = require('../authenticated-page');
|
const { AuthenticatedPage } = require('../authenticated-page');
|
||||||
const { RoleConditionsModal } = require('./role-conditions-modal');
|
const { RoleConditionsModal } = require('./role-conditions-modal');
|
||||||
|
|
||||||
@@ -16,6 +18,7 @@ export class AdminCreateRolePage extends AuthenticatedPage {
|
|||||||
this.executionRow = page.getByTestId('Execution-permission-row');
|
this.executionRow = page.getByTestId('Execution-permission-row');
|
||||||
this.flowRow = page.getByTestId('Flow-permission-row');
|
this.flowRow = page.getByTestId('Flow-permission-row');
|
||||||
this.pageTitle = page.getByTestId('create-role-title');
|
this.pageTitle = page.getByTestId('create-role-title');
|
||||||
|
this.permissionsCatalog = page.getByTestId('permissions-catalog');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -104,4 +107,8 @@ export class AdminCreateRolePage extends AuthenticatedPage {
|
|||||||
throw new Error(`${subject} does not have action ${action}`);
|
throw new Error(`${subject} does not have action ${action}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async waitForPermissionsCatalogToVisible() {
|
||||||
|
await expect(this.permissionsCatalog).toBeVisible();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -14,8 +14,12 @@ export class AdminCreateUserPage extends AuthenticatedPage {
|
|||||||
this.roleInput = page.getByTestId('role.id-autocomplete');
|
this.roleInput = page.getByTestId('role.id-autocomplete');
|
||||||
this.createButton = page.getByTestId('create-button');
|
this.createButton = page.getByTestId('create-button');
|
||||||
this.pageTitle = page.getByTestId('create-user-title');
|
this.pageTitle = page.getByTestId('create-user-title');
|
||||||
this.invitationEmailInfoAlert = page.getByTestId('invitation-email-info-alert');
|
this.invitationEmailInfoAlert = page.getByTestId(
|
||||||
this.acceptInvitationLink = page.getByTestId('invitation-email-info-alert').getByRole('link');
|
'invitation-email-info-alert'
|
||||||
|
);
|
||||||
|
this.acceptInvitationLink = page
|
||||||
|
.getByTestId('invitation-email-info-alert')
|
||||||
|
.getByRole('link');
|
||||||
}
|
}
|
||||||
|
|
||||||
seed(seed) {
|
seed(seed) {
|
||||||
|
@@ -95,7 +95,6 @@ export class AdminUsersPage extends AuthenticatedPage {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
const rowLocator = await this.getUserRowByEmail(email);
|
const rowLocator = await this.getUserRowByEmail(email);
|
||||||
console.log('rowLocator.count', email, await rowLocator.count());
|
|
||||||
if ((await rowLocator.count()) === 1) {
|
if ((await rowLocator.count()) === 1) {
|
||||||
return rowLocator;
|
return rowLocator;
|
||||||
}
|
}
|
||||||
|
@@ -51,10 +51,20 @@ export class BasePage {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async closeSnackbar() {
|
||||||
|
await this.snackbar.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
async closeSnackbarAndWaitUntilDetached() {
|
||||||
|
const snackbar = await this.snackbar;
|
||||||
|
await snackbar.click();
|
||||||
|
await snackbar.waitFor({ state: 'detached' });
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Closes all snackbars, should be replaced later
|
* Closes all snackbars, should be replaced later
|
||||||
*/
|
*/
|
||||||
async closeSnackbar() {
|
async closeAllSnackbars() {
|
||||||
const snackbars = await this.snackbar.all();
|
const snackbars = await this.snackbar.all();
|
||||||
for (const snackbar of snackbars) {
|
for (const snackbar of snackbars) {
|
||||||
await snackbar.click();
|
await snackbar.click();
|
||||||
|
16
packages/e2e-tests/helpers/auth-api-helper.js
Normal file
16
packages/e2e-tests/helpers/auth-api-helper.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
const { expect } = require('../fixtures/index');
|
||||||
|
|
||||||
|
export const getToken = async (apiRequest) => {
|
||||||
|
const tokenResponse = await apiRequest.post(
|
||||||
|
`${process.env.BACKEND_APP_URL}/api/v1/access-tokens`,
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
email: process.env.LOGIN_EMAIL,
|
||||||
|
password: process.env.LOGIN_PASSWORD,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await expect(tokenResponse.status()).toBe(200);
|
||||||
|
|
||||||
|
return await tokenResponse.json();
|
||||||
|
};
|
69
packages/e2e-tests/helpers/flow-api-helper.js
Normal file
69
packages/e2e-tests/helpers/flow-api-helper.js
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
const { expect } = require('../fixtures/index');
|
||||||
|
|
||||||
|
export const createFlow = async (request, token) => {
|
||||||
|
const response = await request.post(
|
||||||
|
`${process.env.BACKEND_APP_URL}/api/v1/flows`,
|
||||||
|
{ headers: { Authorization: token } }
|
||||||
|
);
|
||||||
|
await expect(response.status()).toBe(201);
|
||||||
|
return await response.json();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getFlow = async (request, token, flowId) => {
|
||||||
|
const response = await request.get(
|
||||||
|
`${process.env.BACKEND_APP_URL}/api/v1/flows/${flowId}`,
|
||||||
|
{ headers: { Authorization: token } }
|
||||||
|
);
|
||||||
|
await expect(response.status()).toBe(200);
|
||||||
|
return await response.json();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateFlowName = async (request, token, flowId) => {
|
||||||
|
const updateFlowNameResponse = await request.patch(
|
||||||
|
`${process.env.BACKEND_APP_URL}/api/v1/flows/${flowId}`,
|
||||||
|
{
|
||||||
|
headers: { Authorization: token },
|
||||||
|
data: { name: flowId },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await expect(updateFlowNameResponse.status()).toBe(200);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateFlowStep = async (request, token, stepId, requestBody) => {
|
||||||
|
const updateTriggerStepResponse = await request.patch(
|
||||||
|
`${process.env.BACKEND_APP_URL}/api/v1/steps/${stepId}`,
|
||||||
|
{
|
||||||
|
headers: { Authorization: token },
|
||||||
|
data: requestBody,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await expect(updateTriggerStepResponse.status()).toBe(200);
|
||||||
|
return await updateTriggerStepResponse.json();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const testStep = async (request, token, stepId) => {
|
||||||
|
const testTriggerStepResponse = await request.post(
|
||||||
|
`${process.env.BACKEND_APP_URL}/api/v1/steps/${stepId}/test`,
|
||||||
|
{
|
||||||
|
headers: { Authorization: token },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await expect(testTriggerStepResponse.status()).toBe(200);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const publishFlow = async (request, token, flowId) => {
|
||||||
|
const publishFlowResponse = await request.patch(
|
||||||
|
`${process.env.BACKEND_APP_URL}/api/v1/flows/${flowId}/status`,
|
||||||
|
{
|
||||||
|
headers: { Authorization: token },
|
||||||
|
data: { active: true },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await expect(publishFlowResponse.status()).toBe(200);
|
||||||
|
return publishFlowResponse.json();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const triggerFlow = async (request, url) => {
|
||||||
|
const triggerFlowResponse = await request.get(url);
|
||||||
|
await expect(triggerFlowResponse.status()).toBe(204);
|
||||||
|
};
|
24
packages/e2e-tests/helpers/user-api-helper.js
Normal file
24
packages/e2e-tests/helpers/user-api-helper.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
const { expect } = require('../fixtures/index');
|
||||||
|
|
||||||
|
export const addUser = async (apiRequest, token, request) => {
|
||||||
|
const addUserResponse = await apiRequest.post(
|
||||||
|
`${process.env.BACKEND_APP_URL}/api/v1/admin/users`,
|
||||||
|
{
|
||||||
|
headers: { Authorization: token },
|
||||||
|
data: request,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await expect(addUserResponse.status()).toBe(201);
|
||||||
|
|
||||||
|
return await addUserResponse.json();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const acceptInvitation = async (apiRequest, request) => {
|
||||||
|
const acceptInvitationResponse = await apiRequest.post(
|
||||||
|
`${process.env.BACKEND_APP_URL}/api/v1/users/invitation`,
|
||||||
|
{
|
||||||
|
data: request,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await expect(acceptInvitationResponse.status()).toBe(204);
|
||||||
|
};
|
@@ -1,3 +1,5 @@
|
|||||||
|
import { knexSnakeCaseMappers } from 'objection';
|
||||||
|
|
||||||
const fileExtension = 'js';
|
const fileExtension = 'js';
|
||||||
|
|
||||||
const knexConfig = {
|
const knexConfig = {
|
||||||
@@ -7,7 +9,7 @@ const knexConfig = {
|
|||||||
user: process.env.POSTGRES_USERNAME,
|
user: process.env.POSTGRES_USERNAME,
|
||||||
port: process.env.POSTGRES_PORT,
|
port: process.env.POSTGRES_PORT,
|
||||||
password: process.env.POSTGRES_PASSWORD,
|
password: process.env.POSTGRES_PASSWORD,
|
||||||
database: process.env.POSTGRES_DATABASE
|
database: process.env.POSTGRES_DATABASE,
|
||||||
},
|
},
|
||||||
searchPath: ['public'],
|
searchPath: ['public'],
|
||||||
pool: { min: 0, max: 20 },
|
pool: { min: 0, max: 20 },
|
||||||
|
@@ -26,13 +26,16 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@faker-js/faker": "^8.2.0",
|
"@faker-js/faker": "^8.2.0",
|
||||||
"@playwright/test": "^1.45.1"
|
"@playwright/test": "1.49.0",
|
||||||
|
"objection": "^3.1.5"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"axios": "^1.6.0",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"eslint": "^8.13.0",
|
"eslint": "^8.13.0",
|
||||||
"eslint-config-prettier": "^8.3.0",
|
"eslint-config-prettier": "^8.3.0",
|
||||||
"eslint-plugin-prettier": "^4.0.0",
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
|
"knex": "^2.4.0",
|
||||||
"luxon": "^3.4.4",
|
"luxon": "^3.4.4",
|
||||||
"micro": "^10.0.1",
|
"micro": "^10.0.1",
|
||||||
"pg": "^8.12.0",
|
"pg": "^8.12.0",
|
||||||
|
@@ -15,9 +15,9 @@ module.exports = defineConfig({
|
|||||||
fullyParallel: true,
|
fullyParallel: true,
|
||||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||||
forbidOnly: !!process.env.CI,
|
forbidOnly: !!process.env.CI,
|
||||||
retries: 0,
|
retries: process.env.CI ? 1 : 0,
|
||||||
/* Opt out of parallel tests on CI. */
|
/* Opt out of parallel tests on CI. */
|
||||||
workers: process.env.CI ? 1 : undefined,
|
workers: undefined,
|
||||||
/* Timeout threshold for each test */
|
/* Timeout threshold for each test */
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||||
@@ -30,7 +30,7 @@ module.exports = defineConfig({
|
|||||||
baseURL: process.env.BASE_URL || 'http://localhost:3001',
|
baseURL: process.env.BASE_URL || 'http://localhost:3001',
|
||||||
|
|
||||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||||
trace: 'retain-on-failure',
|
trace: 'on-first-retry',
|
||||||
testIdAttribute: 'data-test',
|
testIdAttribute: 'data-test',
|
||||||
viewport: { width: 1280, height: 720 },
|
viewport: { width: 1280, height: 720 },
|
||||||
},
|
},
|
||||||
|
@@ -5,16 +5,18 @@ test.describe('Admin Applications', () => {
|
|||||||
test.beforeAll(async () => {
|
test.beforeAll(async () => {
|
||||||
const deleteAppAuthClients = {
|
const deleteAppAuthClients = {
|
||||||
text: 'DELETE FROM app_auth_clients WHERE app_key in ($1, $2, $3, $4, $5)',
|
text: 'DELETE FROM app_auth_clients WHERE app_key in ($1, $2, $3, $4, $5)',
|
||||||
values: ['carbone', 'spotify', 'deepl', 'mailchimp', 'reddit']
|
values: ['carbone', 'spotify', 'deepl', 'mailchimp', 'reddit'],
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteAppConfigs = {
|
const deleteAppConfigs = {
|
||||||
text: 'DELETE FROM app_configs WHERE key in ($1, $2, $3, $4, $5)',
|
text: 'DELETE FROM app_configs WHERE key in ($1, $2, $3, $4, $5)',
|
||||||
values: ['carbone', 'spotify', 'deepl', 'mailchimp', 'reddit']
|
values: ['carbone', 'spotify', 'deepl', 'mailchimp', 'reddit'],
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const deleteAppAuthClientsResult = await pgPool.query(deleteAppAuthClients);
|
const deleteAppAuthClientsResult = await pgPool.query(
|
||||||
|
deleteAppAuthClients
|
||||||
|
);
|
||||||
expect(deleteAppAuthClientsResult.command).toBe('DELETE');
|
expect(deleteAppAuthClientsResult.command).toBe('DELETE');
|
||||||
const deleteAppConfigsResult = await pgPool.query(deleteAppConfigs);
|
const deleteAppConfigsResult = await pgPool.query(deleteAppConfigs);
|
||||||
expect(deleteAppConfigsResult.command).toBe('DELETE');
|
expect(deleteAppConfigsResult.command).toBe('DELETE');
|
||||||
@@ -28,10 +30,11 @@ test.describe('Admin Applications', () => {
|
|||||||
await adminApplicationsPage.navigateTo();
|
await adminApplicationsPage.navigateTo();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Admin should be able to toggle Application settings', async ({
|
// TODO skip until https://github.com/automatisch/automatisch/pull/2244
|
||||||
|
test.skip('Admin should be able to toggle Application settings', async ({
|
||||||
adminApplicationsPage,
|
adminApplicationsPage,
|
||||||
adminApplicationSettingsPage,
|
adminApplicationSettingsPage,
|
||||||
page
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
await adminApplicationsPage.openApplication('Carbone');
|
await adminApplicationsPage.openApplication('Carbone');
|
||||||
await expect(page.url()).toContain('/admin-settings/apps/carbone/settings');
|
await expect(page.url()).toContain('/admin-settings/apps/carbone/settings');
|
||||||
@@ -57,7 +60,7 @@ test.describe('Admin Applications', () => {
|
|||||||
adminApplicationsPage,
|
adminApplicationsPage,
|
||||||
adminApplicationSettingsPage,
|
adminApplicationSettingsPage,
|
||||||
flowEditorPage,
|
flowEditorPage,
|
||||||
page
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
await adminApplicationsPage.openApplication('Spotify');
|
await adminApplicationsPage.openApplication('Spotify');
|
||||||
await expect(page.url()).toContain('/admin-settings/apps/spotify/settings');
|
await expect(page.url()).toContain('/admin-settings/apps/spotify/settings');
|
||||||
@@ -75,11 +78,15 @@ test.describe('Admin Applications', () => {
|
|||||||
const triggerStep = flowEditorPage.flowStep.last();
|
const triggerStep = flowEditorPage.flowStep.last();
|
||||||
await triggerStep.click();
|
await triggerStep.click();
|
||||||
|
|
||||||
await flowEditorPage.chooseAppAndEvent("Spotify", "Create Playlist");
|
await flowEditorPage.chooseAppAndEvent('Spotify', 'Create Playlist');
|
||||||
await flowEditorPage.connectionAutocomplete.click();
|
await flowEditorPage.connectionAutocomplete.click();
|
||||||
|
|
||||||
const newConnectionOption = page.getByRole('option').filter({ hasText: 'Add new connection' });
|
const newConnectionOption = page
|
||||||
const newSharedConnectionOption = page.getByRole('option').filter({ hasText: 'Add new shared connection' });
|
.getByRole('option')
|
||||||
|
.filter({ hasText: 'Add new connection' });
|
||||||
|
const newSharedConnectionOption = page
|
||||||
|
.getByRole('option')
|
||||||
|
.filter({ hasText: 'Add new shared connection' });
|
||||||
|
|
||||||
await expect(newConnectionOption).toBeEnabled();
|
await expect(newConnectionOption).toBeEnabled();
|
||||||
await expect(newConnectionOption).toHaveCount(1);
|
await expect(newConnectionOption).toHaveCount(1);
|
||||||
@@ -91,7 +98,7 @@ test.describe('Admin Applications', () => {
|
|||||||
adminApplicationSettingsPage,
|
adminApplicationSettingsPage,
|
||||||
adminApplicationAuthClientsPage,
|
adminApplicationAuthClientsPage,
|
||||||
flowEditorPage,
|
flowEditorPage,
|
||||||
page
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
await adminApplicationsPage.openApplication('Reddit');
|
await adminApplicationsPage.openApplication('Reddit');
|
||||||
await expect(page.url()).toContain('/admin-settings/apps/reddit/settings');
|
await expect(page.url()).toContain('/admin-settings/apps/reddit/settings');
|
||||||
@@ -101,13 +108,21 @@ test.describe('Admin Applications', () => {
|
|||||||
|
|
||||||
await adminApplicationAuthClientsPage.openAuthClientsTab();
|
await adminApplicationAuthClientsPage.openAuthClientsTab();
|
||||||
await adminApplicationAuthClientsPage.openFirstAuthClientCreateForm();
|
await adminApplicationAuthClientsPage.openFirstAuthClientCreateForm();
|
||||||
const authClientForm = page.getByTestId("auth-client-form");
|
const authClientForm = page.getByTestId('auth-client-form');
|
||||||
await authClientForm.locator(page.getByTestId('switch')).check();
|
await authClientForm.locator(page.getByTestId('switch')).check();
|
||||||
await authClientForm.locator(page.locator('[name="name"]')).fill('redditAuthClient');
|
await authClientForm
|
||||||
await authClientForm.locator(page.locator('[name="clientId"]')).fill('redditClientId');
|
.locator(page.locator('[name="name"]'))
|
||||||
await authClientForm.locator(page.locator('[name="clientSecret"]')).fill('redditClientSecret');
|
.fill('redditAuthClient');
|
||||||
|
await authClientForm
|
||||||
|
.locator(page.locator('[name="clientId"]'))
|
||||||
|
.fill('redditClientId');
|
||||||
|
await authClientForm
|
||||||
|
.locator(page.locator('[name="clientSecret"]'))
|
||||||
|
.fill('redditClientSecret');
|
||||||
await adminApplicationAuthClientsPage.submitAuthClientForm();
|
await adminApplicationAuthClientsPage.submitAuthClientForm();
|
||||||
await adminApplicationAuthClientsPage.authClientShouldBeVisible('redditAuthClient');
|
await adminApplicationAuthClientsPage.authClientShouldBeVisible(
|
||||||
|
'redditAuthClient'
|
||||||
|
);
|
||||||
|
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
await page.getByTestId('create-flow-button').click();
|
await page.getByTestId('create-flow-button').click();
|
||||||
@@ -119,11 +134,15 @@ test.describe('Admin Applications', () => {
|
|||||||
const triggerStep = flowEditorPage.flowStep.last();
|
const triggerStep = flowEditorPage.flowStep.last();
|
||||||
await triggerStep.click();
|
await triggerStep.click();
|
||||||
|
|
||||||
await flowEditorPage.chooseAppAndEvent("Reddit", "Create link post");
|
await flowEditorPage.chooseAppAndEvent('Reddit', 'Create link post');
|
||||||
await flowEditorPage.connectionAutocomplete.click();
|
await flowEditorPage.connectionAutocomplete.click();
|
||||||
|
|
||||||
const newConnectionOption = page.getByRole('option').filter({ hasText: 'Add new connection' });
|
const newConnectionOption = page
|
||||||
const newSharedConnectionOption = page.getByRole('option').filter({ hasText: 'Add new shared connection' });
|
.getByRole('option')
|
||||||
|
.filter({ hasText: 'Add new connection' });
|
||||||
|
const newSharedConnectionOption = page
|
||||||
|
.getByRole('option')
|
||||||
|
.filter({ hasText: 'Add new shared connection' });
|
||||||
|
|
||||||
await expect(newConnectionOption).toHaveCount(0);
|
await expect(newConnectionOption).toHaveCount(0);
|
||||||
await expect(newSharedConnectionOption).toBeEnabled();
|
await expect(newSharedConnectionOption).toBeEnabled();
|
||||||
@@ -134,7 +153,7 @@ test.describe('Admin Applications', () => {
|
|||||||
adminApplicationsPage,
|
adminApplicationsPage,
|
||||||
adminApplicationSettingsPage,
|
adminApplicationSettingsPage,
|
||||||
flowEditorPage,
|
flowEditorPage,
|
||||||
page
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
await adminApplicationsPage.openApplication('DeepL');
|
await adminApplicationsPage.openApplication('DeepL');
|
||||||
await expect(page.url()).toContain('/admin-settings/apps/deepl/settings');
|
await expect(page.url()).toContain('/admin-settings/apps/deepl/settings');
|
||||||
@@ -152,12 +171,18 @@ test.describe('Admin Applications', () => {
|
|||||||
const triggerStep = flowEditorPage.flowStep.last();
|
const triggerStep = flowEditorPage.flowStep.last();
|
||||||
await triggerStep.click();
|
await triggerStep.click();
|
||||||
|
|
||||||
await flowEditorPage.chooseAppAndEvent("DeepL", "Translate text");
|
await flowEditorPage.chooseAppAndEvent('DeepL', 'Translate text');
|
||||||
await flowEditorPage.connectionAutocomplete.click();
|
await flowEditorPage.connectionAutocomplete.click();
|
||||||
|
|
||||||
const newConnectionOption = page.getByRole('option').filter({ hasText: 'Add new connection' });
|
const newConnectionOption = page
|
||||||
const newSharedConnectionOption = page.getByRole('option').filter({ hasText: 'Add new shared connection' });
|
.getByRole('option')
|
||||||
const noConnectionsOption = page.locator('.MuiAutocomplete-noOptions').filter({ hasText: 'No options' });
|
.filter({ hasText: 'Add new connection' });
|
||||||
|
const newSharedConnectionOption = page
|
||||||
|
.getByRole('option')
|
||||||
|
.filter({ hasText: 'Add new shared connection' });
|
||||||
|
const noConnectionsOption = page
|
||||||
|
.locator('.MuiAutocomplete-noOptions')
|
||||||
|
.filter({ hasText: 'No options' });
|
||||||
|
|
||||||
await expect(noConnectionsOption).toHaveCount(1);
|
await expect(noConnectionsOption).toHaveCount(1);
|
||||||
await expect(newConnectionOption).toHaveCount(0);
|
await expect(newConnectionOption).toHaveCount(0);
|
||||||
@@ -168,11 +193,11 @@ test.describe('Admin Applications', () => {
|
|||||||
adminApplicationsPage,
|
adminApplicationsPage,
|
||||||
adminApplicationSettingsPage,
|
adminApplicationSettingsPage,
|
||||||
flowEditorPage,
|
flowEditorPage,
|
||||||
page
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
const queryUser = {
|
const queryUser = {
|
||||||
text: 'SELECT * FROM users WHERE email = $1',
|
text: 'SELECT * FROM users WHERE email = $1',
|
||||||
values: [process.env.LOGIN_EMAIL]
|
values: [process.env.LOGIN_EMAIL],
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -183,14 +208,16 @@ test.describe('Admin Applications', () => {
|
|||||||
text: 'INSERT INTO connections (key, data, user_id, verified, draft) VALUES ($1, $2, $3, $4, $5)',
|
text: 'INSERT INTO connections (key, data, user_id, verified, draft) VALUES ($1, $2, $3, $4, $5)',
|
||||||
values: [
|
values: [
|
||||||
'mailchimp',
|
'mailchimp',
|
||||||
"U2FsdGVkX1+cAtdHwLiuRL4DaK/T1aljeeKyPMmtWK0AmAIsKhYwQiuyQCYJO3mdZ31z73hqF2Y+yj2Kn2/IIpLRqCxB2sC0rCDCZyolzOZ290YcBXSzYRzRUxhoOcZEtwYDKsy8AHygKK/tkj9uv9k6wOe1LjipNik4VmRhKjEYizzjLrJpbeU1oY+qW0GBpPYomFTeNf+MejSSmsUYyYJ8+E/4GeEfaonvsTSwMT7AId98Lck6Vy4wrfgpm7sZZ8xU15/HqXZNc8UCo2iTdw45xj/Oov9+brX4WUASFPG8aYrK8dl/EdaOvr89P8uIofbSNZ25GjJvVF5ymarrPkTZ7djjJXchzpwBY+7GTJfs3funR/vIk0Hq95jgOFFP1liZyqTXSa49ojG3hzojRQ==",
|
'U2FsdGVkX1+cAtdHwLiuRL4DaK/T1aljeeKyPMmtWK0AmAIsKhYwQiuyQCYJO3mdZ31z73hqF2Y+yj2Kn2/IIpLRqCxB2sC0rCDCZyolzOZ290YcBXSzYRzRUxhoOcZEtwYDKsy8AHygKK/tkj9uv9k6wOe1LjipNik4VmRhKjEYizzjLrJpbeU1oY+qW0GBpPYomFTeNf+MejSSmsUYyYJ8+E/4GeEfaonvsTSwMT7AId98Lck6Vy4wrfgpm7sZZ8xU15/HqXZNc8UCo2iTdw45xj/Oov9+brX4WUASFPG8aYrK8dl/EdaOvr89P8uIofbSNZ25GjJvVF5ymarrPkTZ7djjJXchzpwBY+7GTJfs3funR/vIk0Hq95jgOFFP1liZyqTXSa49ojG3hzojRQ==',
|
||||||
queryUserResult.rows[0].id,
|
queryUserResult.rows[0].id,
|
||||||
'true',
|
'true',
|
||||||
'false'
|
'false',
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const createMailchimpConnectionResult = await pgPool.query(createMailchimpConnection);
|
const createMailchimpConnectionResult = await pgPool.query(
|
||||||
|
createMailchimpConnection
|
||||||
|
);
|
||||||
expect(createMailchimpConnectionResult.rowCount).toBe(1);
|
expect(createMailchimpConnectionResult.rowCount).toBe(1);
|
||||||
expect(createMailchimpConnectionResult.command).toBe('INSERT');
|
expect(createMailchimpConnectionResult.command).toBe('INSERT');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -199,7 +226,9 @@ test.describe('Admin Applications', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await adminApplicationsPage.openApplication('Mailchimp');
|
await adminApplicationsPage.openApplication('Mailchimp');
|
||||||
await expect(page.url()).toContain('/admin-settings/apps/mailchimp/settings');
|
await expect(page.url()).toContain(
|
||||||
|
'/admin-settings/apps/mailchimp/settings'
|
||||||
|
);
|
||||||
|
|
||||||
await adminApplicationSettingsPage.disallowConnections();
|
await adminApplicationSettingsPage.disallowConnections();
|
||||||
await adminApplicationSettingsPage.expectSuccessSnackbarToBeVisible();
|
await adminApplicationSettingsPage.expectSuccessSnackbarToBeVisible();
|
||||||
@@ -214,14 +243,22 @@ test.describe('Admin Applications', () => {
|
|||||||
const triggerStep = flowEditorPage.flowStep.last();
|
const triggerStep = flowEditorPage.flowStep.last();
|
||||||
await triggerStep.click();
|
await triggerStep.click();
|
||||||
|
|
||||||
await flowEditorPage.chooseAppAndEvent("Mailchimp", "Create campaign");
|
await flowEditorPage.chooseAppAndEvent('Mailchimp', 'Create campaign');
|
||||||
await flowEditorPage.connectionAutocomplete.click();
|
await flowEditorPage.connectionAutocomplete.click();
|
||||||
await expect(page.getByRole('option').first()).toHaveText('Unnamed');
|
await expect(page.getByRole('option').first()).toHaveText('Unnamed');
|
||||||
|
|
||||||
const existingConnection = page.getByRole('option').filter({ hasText: 'Unnamed' });
|
const existingConnection = page
|
||||||
const newConnectionOption = page.getByRole('option').filter({ hasText: 'Add new connection' });
|
.getByRole('option')
|
||||||
const newSharedConnectionOption = page.getByRole('option').filter({ hasText: 'Add new shared connection' });
|
.filter({ hasText: 'Unnamed' });
|
||||||
const noConnectionsOption = page.locator('.MuiAutocomplete-noOptions').filter({ hasText: 'No options' });
|
const newConnectionOption = page
|
||||||
|
.getByRole('option')
|
||||||
|
.filter({ hasText: 'Add new connection' });
|
||||||
|
const newSharedConnectionOption = page
|
||||||
|
.getByRole('option')
|
||||||
|
.filter({ hasText: 'Add new shared connection' });
|
||||||
|
const noConnectionsOption = page
|
||||||
|
.locator('.MuiAutocomplete-noOptions')
|
||||||
|
.filter({ hasText: 'No options' });
|
||||||
|
|
||||||
await expect(await existingConnection.count()).toBeGreaterThan(0);
|
await expect(await existingConnection.count()).toBeGreaterThan(0);
|
||||||
await expect(noConnectionsOption).toHaveCount(0);
|
await expect(noConnectionsOption).toHaveCount(0);
|
||||||
|
@@ -22,22 +22,18 @@ test.describe('Role management page', () => {
|
|||||||
await adminRolesPage.navigateTo();
|
await adminRolesPage.navigateTo();
|
||||||
await adminRolesPage.createRoleButton.click();
|
await adminRolesPage.createRoleButton.click();
|
||||||
await adminCreateRolePage.isMounted();
|
await adminCreateRolePage.isMounted();
|
||||||
|
await adminCreateRolePage.waitForPermissionsCatalogToVisible();
|
||||||
await adminCreateRolePage.nameInput.fill('Create Edit Test');
|
await adminCreateRolePage.nameInput.fill('Create Edit Test');
|
||||||
await adminCreateRolePage.descriptionInput.fill('Test description');
|
await adminCreateRolePage.descriptionInput.fill('Test description');
|
||||||
await adminCreateRolePage.createButton.click();
|
await adminCreateRolePage.createButton.click();
|
||||||
await adminCreateRolePage.snackbar.waitFor({
|
|
||||||
state: 'attached',
|
|
||||||
});
|
|
||||||
const snackbar = await adminCreateRolePage.getSnackbarData(
|
const snackbar = await adminCreateRolePage.getSnackbarData(
|
||||||
'snackbar-create-role-success'
|
'snackbar-create-role-success'
|
||||||
);
|
);
|
||||||
await expect(snackbar.variant).toBe('success');
|
await expect(snackbar.variant).toBe('success');
|
||||||
await adminCreateRolePage.closeSnackbar();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let roleRow = await test.step(
|
let roleRow =
|
||||||
'Make sure role data is correct',
|
await test.step('Make sure role data is correct', async () => {
|
||||||
async () => {
|
|
||||||
const roleRow = await adminRolesPage.getRoleRowByName(
|
const roleRow = await adminRolesPage.getRoleRowByName(
|
||||||
'Create Edit Test'
|
'Create Edit Test'
|
||||||
);
|
);
|
||||||
@@ -48,8 +44,7 @@ test.describe('Role management page', () => {
|
|||||||
await expect(roleData.canEdit).toBe(true);
|
await expect(roleData.canEdit).toBe(true);
|
||||||
await expect(roleData.canDelete).toBe(true);
|
await expect(roleData.canDelete).toBe(true);
|
||||||
return roleRow;
|
return roleRow;
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
await test.step('Edit the role', async () => {
|
await test.step('Edit the role', async () => {
|
||||||
await adminRolesPage.clickEditRole(roleRow);
|
await adminRolesPage.clickEditRole(roleRow);
|
||||||
@@ -57,19 +52,14 @@ test.describe('Role management page', () => {
|
|||||||
await adminEditRolePage.nameInput.fill('Create Update Test');
|
await adminEditRolePage.nameInput.fill('Create Update Test');
|
||||||
await adminEditRolePage.descriptionInput.fill('Update test description');
|
await adminEditRolePage.descriptionInput.fill('Update test description');
|
||||||
await adminEditRolePage.updateButton.click();
|
await adminEditRolePage.updateButton.click();
|
||||||
await adminEditRolePage.snackbar.waitFor({
|
|
||||||
state: 'attached',
|
|
||||||
});
|
|
||||||
const snackbar = await adminEditRolePage.getSnackbarData(
|
const snackbar = await adminEditRolePage.getSnackbarData(
|
||||||
'snackbar-edit-role-success'
|
'snackbar-edit-role-success'
|
||||||
);
|
);
|
||||||
await expect(snackbar.variant).toBe('success');
|
await expect(snackbar.variant).toBe('success');
|
||||||
await adminEditRolePage.closeSnackbar();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
roleRow = await test.step(
|
roleRow =
|
||||||
'Make sure changes reflected on roles page',
|
await test.step('Make sure changes reflected on roles page', async () => {
|
||||||
async () => {
|
|
||||||
await adminRolesPage.isMounted();
|
await adminRolesPage.isMounted();
|
||||||
const roleRow = await adminRolesPage.getRoleRowByName(
|
const roleRow = await adminRolesPage.getRoleRowByName(
|
||||||
'Create Update Test'
|
'Create Update Test'
|
||||||
@@ -81,8 +71,7 @@ test.describe('Role management page', () => {
|
|||||||
await expect(roleData.canEdit).toBe(true);
|
await expect(roleData.canEdit).toBe(true);
|
||||||
await expect(roleData.canDelete).toBe(true);
|
await expect(roleData.canDelete).toBe(true);
|
||||||
return roleRow;
|
return roleRow;
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
await test.step('Delete the role', async () => {
|
await test.step('Delete the role', async () => {
|
||||||
await adminRolesPage.clickDeleteRole(roleRow);
|
await adminRolesPage.clickDeleteRole(roleRow);
|
||||||
@@ -91,14 +80,10 @@ test.describe('Role management page', () => {
|
|||||||
state: 'attached',
|
state: 'attached',
|
||||||
});
|
});
|
||||||
await deleteModal.deleteButton.click();
|
await deleteModal.deleteButton.click();
|
||||||
await adminRolesPage.snackbar.waitFor({
|
|
||||||
state: 'attached',
|
|
||||||
});
|
|
||||||
const snackbar = await adminRolesPage.getSnackbarData(
|
const snackbar = await adminRolesPage.getSnackbarData(
|
||||||
'snackbar-delete-role-success'
|
'snackbar-delete-role-success'
|
||||||
);
|
);
|
||||||
await expect(snackbar.variant).toBe('success');
|
await expect(snackbar.variant).toBe('success');
|
||||||
await adminRolesPage.closeSnackbar();
|
|
||||||
await deleteModal.modal.waitFor({
|
await deleteModal.modal.waitFor({
|
||||||
state: 'detached',
|
state: 'detached',
|
||||||
});
|
});
|
||||||
@@ -173,60 +158,45 @@ test.describe('Role management page', () => {
|
|||||||
await test.step('Create a new role', async () => {
|
await test.step('Create a new role', async () => {
|
||||||
await adminRolesPage.createRoleButton.click();
|
await adminRolesPage.createRoleButton.click();
|
||||||
await adminCreateRolePage.isMounted();
|
await adminCreateRolePage.isMounted();
|
||||||
|
await adminCreateRolePage.waitForPermissionsCatalogToVisible();
|
||||||
await adminCreateRolePage.nameInput.fill('Delete Role');
|
await adminCreateRolePage.nameInput.fill('Delete Role');
|
||||||
await adminCreateRolePage.createButton.click();
|
await adminCreateRolePage.createButton.click();
|
||||||
await adminCreateRolePage.snackbar.waitFor({
|
|
||||||
state: 'attached',
|
|
||||||
});
|
|
||||||
const snackbar = await adminCreateRolePage.getSnackbarData(
|
const snackbar = await adminCreateRolePage.getSnackbarData(
|
||||||
'snackbar-create-role-success'
|
'snackbar-create-role-success'
|
||||||
);
|
);
|
||||||
await expect(snackbar.variant).toBe('success');
|
await expect(snackbar.variant).toBe('success');
|
||||||
await adminCreateRolePage.closeSnackbar();
|
|
||||||
});
|
});
|
||||||
await test.step(
|
await test.step('Create a new user with the "Delete Role" role', async () => {
|
||||||
'Create a new user with the "Delete Role" role',
|
await adminUsersPage.navigateTo();
|
||||||
async () => {
|
await adminUsersPage.createUserButton.click();
|
||||||
await adminUsersPage.navigateTo();
|
await adminCreateUserPage.fullNameInput.fill('User Role Test');
|
||||||
await adminUsersPage.createUserButton.click();
|
await adminCreateUserPage.emailInput.fill(
|
||||||
await adminCreateUserPage.fullNameInput.fill('User Role Test');
|
'user-role-test@automatisch.io'
|
||||||
await adminCreateUserPage.emailInput.fill(
|
);
|
||||||
'user-role-test@automatisch.io'
|
await adminCreateUserPage.roleInput.click();
|
||||||
);
|
await adminCreateUserPage.page
|
||||||
await adminCreateUserPage.roleInput.click();
|
.getByRole('option', { name: 'Delete Role', exact: true })
|
||||||
await adminCreateUserPage.page
|
.click();
|
||||||
.getByRole('option', { name: 'Delete Role', exact: true })
|
await adminCreateUserPage.createButton.click();
|
||||||
.click();
|
await adminCreateUserPage.invitationEmailInfoAlert.waitFor({
|
||||||
await adminCreateUserPage.createButton.click();
|
state: 'attached',
|
||||||
await adminCreateUserPage.snackbar.waitFor({
|
});
|
||||||
state: 'attached',
|
const snackbar = await adminUsersPage.getSnackbarData(
|
||||||
});
|
'snackbar-create-user-success'
|
||||||
await adminCreateUserPage.invitationEmailInfoAlert.waitFor({
|
);
|
||||||
state: 'attached',
|
await expect(snackbar.variant).toBe('success');
|
||||||
});
|
});
|
||||||
const snackbar = await adminUsersPage.getSnackbarData(
|
await test.step('Try to delete "Delete Role" role when new user has it', async () => {
|
||||||
'snackbar-create-user-success'
|
await adminRolesPage.navigateTo();
|
||||||
);
|
const row = await adminRolesPage.getRoleRowByName('Delete Role');
|
||||||
await expect(snackbar.variant).toBe('success');
|
const modal = await adminRolesPage.clickDeleteRole(row);
|
||||||
await adminUsersPage.closeSnackbar();
|
await modal.deleteButton.click();
|
||||||
}
|
const snackbar = await adminRolesPage.getSnackbarData(
|
||||||
);
|
'snackbar-delete-role-error'
|
||||||
await test.step(
|
);
|
||||||
'Try to delete "Delete Role" role when new user has it',
|
await expect(snackbar.variant).toBe('error');
|
||||||
async () => {
|
await modal.close();
|
||||||
await adminRolesPage.navigateTo();
|
});
|
||||||
const row = await adminRolesPage.getRoleRowByName('Delete Role');
|
|
||||||
const modal = await adminRolesPage.clickDeleteRole(row);
|
|
||||||
await modal.deleteButton.click();
|
|
||||||
await adminRolesPage.snackbar.waitFor({
|
|
||||||
state: 'attached',
|
|
||||||
});
|
|
||||||
const snackbar = await adminRolesPage.getSnackbarData('snackbar-delete-role-error');
|
|
||||||
await expect(snackbar.variant).toBe('error');
|
|
||||||
await adminRolesPage.closeSnackbar();
|
|
||||||
await modal.close();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
await test.step('Change the role the user has', async () => {
|
await test.step('Change the role the user has', async () => {
|
||||||
await adminUsersPage.navigateTo();
|
await adminUsersPage.navigateTo();
|
||||||
await adminUsersPage.usersLoader.waitFor({
|
await adminUsersPage.usersLoader.waitFor({
|
||||||
@@ -241,14 +211,10 @@ test.describe('Role management page', () => {
|
|||||||
.getByRole('option', { name: 'Admin' })
|
.getByRole('option', { name: 'Admin' })
|
||||||
.click();
|
.click();
|
||||||
await adminEditUserPage.updateButton.click();
|
await adminEditUserPage.updateButton.click();
|
||||||
await adminEditUserPage.snackbar.waitFor({
|
|
||||||
state: 'attached',
|
|
||||||
});
|
|
||||||
const snackbar = await adminEditUserPage.getSnackbarData(
|
const snackbar = await adminEditUserPage.getSnackbarData(
|
||||||
'snackbar-edit-user-success'
|
'snackbar-edit-user-success'
|
||||||
);
|
);
|
||||||
await expect(snackbar.variant).toBe('success');
|
await expect(snackbar.variant).toBe('success');
|
||||||
await adminEditUserPage.closeSnackbar();
|
|
||||||
});
|
});
|
||||||
await test.step('Delete the original role', async () => {
|
await test.step('Delete the original role', async () => {
|
||||||
await adminRolesPage.navigateTo();
|
await adminRolesPage.navigateTo();
|
||||||
@@ -256,14 +222,10 @@ test.describe('Role management page', () => {
|
|||||||
const modal = await adminRolesPage.clickDeleteRole(row);
|
const modal = await adminRolesPage.clickDeleteRole(row);
|
||||||
await expect(modal.modal).toBeVisible();
|
await expect(modal.modal).toBeVisible();
|
||||||
await modal.deleteButton.click();
|
await modal.deleteButton.click();
|
||||||
await adminRolesPage.snackbar.waitFor({
|
|
||||||
state: 'attached',
|
|
||||||
});
|
|
||||||
const snackbar = await adminRolesPage.getSnackbarData(
|
const snackbar = await adminRolesPage.getSnackbarData(
|
||||||
'snackbar-delete-role-success'
|
'snackbar-delete-role-success'
|
||||||
);
|
);
|
||||||
await expect(snackbar.variant).toBe('success');
|
await expect(snackbar.variant).toBe('success');
|
||||||
await adminRolesPage.closeSnackbar();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -277,16 +239,13 @@ test.describe('Role management page', () => {
|
|||||||
await test.step('Create a new role', async () => {
|
await test.step('Create a new role', async () => {
|
||||||
await adminRolesPage.createRoleButton.click();
|
await adminRolesPage.createRoleButton.click();
|
||||||
await adminCreateRolePage.isMounted();
|
await adminCreateRolePage.isMounted();
|
||||||
|
await adminCreateRolePage.waitForPermissionsCatalogToVisible();
|
||||||
await adminCreateRolePage.nameInput.fill('Cannot Delete Role');
|
await adminCreateRolePage.nameInput.fill('Cannot Delete Role');
|
||||||
await adminCreateRolePage.createButton.click();
|
await adminCreateRolePage.createButton.click();
|
||||||
await adminCreateRolePage.snackbar.waitFor({
|
|
||||||
state: 'attached',
|
|
||||||
});
|
|
||||||
const snackbar = await adminCreateRolePage.getSnackbarData(
|
const snackbar = await adminCreateRolePage.getSnackbarData(
|
||||||
'snackbar-create-role-success'
|
'snackbar-create-role-success'
|
||||||
);
|
);
|
||||||
await expect(snackbar.variant).toBe('success');
|
await expect(snackbar.variant).toBe('success');
|
||||||
await adminCreateRolePage.closeSnackbar();
|
|
||||||
});
|
});
|
||||||
await test.step('Create a new user with this role', async () => {
|
await test.step('Create a new user with this role', async () => {
|
||||||
await adminUsersPage.navigateTo();
|
await adminUsersPage.navigateTo();
|
||||||
@@ -301,9 +260,6 @@ test.describe('Role management page', () => {
|
|||||||
.getByRole('option', { name: 'Cannot Delete Role' })
|
.getByRole('option', { name: 'Cannot Delete Role' })
|
||||||
.click();
|
.click();
|
||||||
await adminCreateUserPage.createButton.click();
|
await adminCreateUserPage.createButton.click();
|
||||||
await adminCreateUserPage.snackbar.waitFor({
|
|
||||||
state: 'attached',
|
|
||||||
});
|
|
||||||
await adminCreateUserPage.invitationEmailInfoAlert.waitFor({
|
await adminCreateUserPage.invitationEmailInfoAlert.waitFor({
|
||||||
state: 'attached',
|
state: 'attached',
|
||||||
});
|
});
|
||||||
@@ -311,40 +267,34 @@ test.describe('Role management page', () => {
|
|||||||
'snackbar-create-user-success'
|
'snackbar-create-user-success'
|
||||||
);
|
);
|
||||||
await expect(snackbar.variant).toBe('success');
|
await expect(snackbar.variant).toBe('success');
|
||||||
await adminCreateUserPage.closeSnackbar();
|
|
||||||
});
|
});
|
||||||
await test.step('Delete this user', async () => {
|
await test.step('Delete this user', async () => {
|
||||||
await adminUsersPage.navigateTo();
|
await adminUsersPage.navigateTo();
|
||||||
const row = await adminUsersPage.findUserPageWithEmail(
|
const row = await adminUsersPage.findUserPageWithEmail(
|
||||||
'user-delete-role-test@automatisch.io'
|
'user-delete-role-test@automatisch.io'
|
||||||
);
|
);
|
||||||
// await test.waitForTimeout(10000);
|
|
||||||
const modal = await adminUsersPage.clickDeleteUser(row);
|
const modal = await adminUsersPage.clickDeleteUser(row);
|
||||||
await modal.deleteButton.click();
|
await modal.deleteButton.click();
|
||||||
await adminUsersPage.snackbar.waitFor({
|
|
||||||
state: 'attached',
|
|
||||||
});
|
|
||||||
const snackbar = await adminUsersPage.getSnackbarData(
|
const snackbar = await adminUsersPage.getSnackbarData(
|
||||||
'snackbar-delete-user-success'
|
'snackbar-delete-user-success'
|
||||||
);
|
);
|
||||||
await expect(snackbar.variant).toBe('success');
|
await expect(snackbar.variant).toBe('success');
|
||||||
await adminUsersPage.closeSnackbar();
|
|
||||||
});
|
});
|
||||||
await test.step('Try deleting this role', async () => {
|
await test.step('Try deleting this role', async () => {
|
||||||
await adminRolesPage.navigateTo();
|
await adminRolesPage.navigateTo();
|
||||||
const row = await adminRolesPage.getRoleRowByName('Cannot Delete Role');
|
const row = await adminRolesPage.getRoleRowByName('Cannot Delete Role');
|
||||||
const modal = await adminRolesPage.clickDeleteRole(row);
|
const modal = await adminRolesPage.clickDeleteRole(row);
|
||||||
await modal.deleteButton.click();
|
await modal.deleteButton.click();
|
||||||
await adminRolesPage.snackbar.waitFor({
|
const snackbar = await adminRolesPage.getSnackbarData(
|
||||||
state: 'attached',
|
'snackbar-delete-role-error'
|
||||||
});
|
);
|
||||||
|
await expect(snackbar.variant).toBe('error');
|
||||||
/*
|
/*
|
||||||
* TODO: await snackbar - make assertions based on product
|
* TODO: await snackbar - make assertions based on product
|
||||||
* decisions
|
* decisions
|
||||||
const snackbar = await adminRolesPage.getSnackbarData();
|
const snackbar = await adminRolesPage.getSnackbarData();
|
||||||
await expect(snackbar.variant).toBe('...');
|
await expect(snackbar.variant).toBe('...');
|
||||||
*/
|
*/
|
||||||
await adminRolesPage.closeSnackbar();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -362,16 +312,13 @@ test('Accessibility of role management page', async ({
|
|||||||
await adminRolesPage.navigateTo();
|
await adminRolesPage.navigateTo();
|
||||||
await adminRolesPage.createRoleButton.click();
|
await adminRolesPage.createRoleButton.click();
|
||||||
await adminCreateRolePage.isMounted();
|
await adminCreateRolePage.isMounted();
|
||||||
|
await adminCreateRolePage.waitForPermissionsCatalogToVisible();
|
||||||
await adminCreateRolePage.nameInput.fill('Basic Test');
|
await adminCreateRolePage.nameInput.fill('Basic Test');
|
||||||
await adminCreateRolePage.createButton.click();
|
await adminCreateRolePage.createButton.click();
|
||||||
await adminCreateRolePage.snackbar.waitFor({
|
|
||||||
state: 'attached',
|
|
||||||
});
|
|
||||||
const snackbar = await adminCreateRolePage.getSnackbarData(
|
const snackbar = await adminCreateRolePage.getSnackbarData(
|
||||||
'snackbar-create-role-success'
|
'snackbar-create-role-success'
|
||||||
);
|
);
|
||||||
await expect(snackbar.variant).toBe('success');
|
await expect(snackbar.variant).toBe('success');
|
||||||
await adminCreateRolePage.closeSnackbar();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await test.step('Create a new user with the basic role', async () => {
|
await test.step('Create a new user with the basic role', async () => {
|
||||||
@@ -385,9 +332,6 @@ test('Accessibility of role management page', async ({
|
|||||||
.getByRole('option', { name: 'Basic Test' })
|
.getByRole('option', { name: 'Basic Test' })
|
||||||
.click();
|
.click();
|
||||||
await adminCreateUserPage.createButton.click();
|
await adminCreateUserPage.createButton.click();
|
||||||
await adminCreateUserPage.snackbar.waitFor({
|
|
||||||
state: 'attached',
|
|
||||||
});
|
|
||||||
await adminCreateUserPage.invitationEmailInfoAlert.waitFor({
|
await adminCreateUserPage.invitationEmailInfoAlert.waitFor({
|
||||||
state: 'attached',
|
state: 'attached',
|
||||||
});
|
});
|
||||||
@@ -395,56 +339,46 @@ test('Accessibility of role management page', async ({
|
|||||||
'snackbar-create-user-success'
|
'snackbar-create-user-success'
|
||||||
);
|
);
|
||||||
await expect(snackbar.variant).toBe('success');
|
await expect(snackbar.variant).toBe('success');
|
||||||
await adminCreateUserPage.closeSnackbar();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await test.step('Logout and login to the basic role user', async () => {
|
await test.step('Logout and login to the basic role user', async () => {
|
||||||
const acceptInvitationLink = await adminCreateUserPage.acceptInvitationLink;
|
const acceptInvitationLink = await adminCreateUserPage.acceptInvitationLink;
|
||||||
console.log(acceptInvitationLink);
|
|
||||||
const acceptInvitationUrl = await acceptInvitationLink.textContent();
|
const acceptInvitationUrl = await acceptInvitationLink.textContent();
|
||||||
console.log(acceptInvitationUrl);
|
|
||||||
const acceptInvitatonToken = acceptInvitationUrl.split('?token=')[1];
|
const acceptInvitatonToken = acceptInvitationUrl.split('?token=')[1];
|
||||||
|
|
||||||
await page.getByTestId('profile-menu-button').click();
|
await page.getByTestId('profile-menu-button').click();
|
||||||
await page.getByTestId('logout-item').click();
|
await page.getByTestId('logout-item').click();
|
||||||
|
|
||||||
const acceptInvitationPage = new AcceptInvitation(page);
|
const acceptInvitationPage = new AcceptInvitation(page);
|
||||||
|
|
||||||
await acceptInvitationPage.open(acceptInvitatonToken);
|
await acceptInvitationPage.open(acceptInvitatonToken);
|
||||||
|
|
||||||
await acceptInvitationPage.acceptInvitation('sample');
|
await acceptInvitationPage.acceptInvitation('sample');
|
||||||
|
|
||||||
const loginPage = new LoginPage(page);
|
const loginPage = new LoginPage(page);
|
||||||
|
|
||||||
// await loginPage.isMounted();
|
|
||||||
await loginPage.login('basic-role-test@automatisch.io', 'sample');
|
await loginPage.login('basic-role-test@automatisch.io', 'sample');
|
||||||
await expect(loginPage.loginButton).not.toBeVisible();
|
await expect(loginPage.loginButton).not.toBeVisible();
|
||||||
await expect(page).toHaveURL('/flows');
|
await expect(page).toHaveURL('/flows');
|
||||||
});
|
});
|
||||||
|
|
||||||
await test.step(
|
await test.step('Navigate to the admin settings page and make sure it is blank', async () => {
|
||||||
'Navigate to the admin settings page and make sure it is blank',
|
const pageUrl = new URL(page.url());
|
||||||
async () => {
|
const url = `${pageUrl.origin}/admin-settings/users`;
|
||||||
const pageUrl = new URL(page.url());
|
await page.goto(url);
|
||||||
const url = `${pageUrl.origin}/admin-settings/users`;
|
await page.waitForTimeout(750);
|
||||||
await page.goto(url);
|
const isUnmounted = await page.evaluate(() => {
|
||||||
await page.waitForTimeout(750);
|
// eslint-disable-next-line no-undef
|
||||||
const isUnmounted = await page.evaluate(() => {
|
const root = document.querySelector('#root');
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
const root = document.querySelector('#root');
|
|
||||||
|
|
||||||
if (root) {
|
if (root) {
|
||||||
// We have react query devtools only in dev env.
|
// We have react query devtools only in dev env.
|
||||||
// In production, there is nothing in root.
|
// In production, there is nothing in root.
|
||||||
// That's why `<= 1`.
|
// That's why `<= 1`.
|
||||||
return root.children.length <= 1;
|
return root.children.length <= 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
await expect(isUnmounted).toBe(true);
|
await expect(isUnmounted).toBe(true);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
await test.step('Log back into the admin account', async () => {
|
await test.step('Log back into the admin account', async () => {
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
@@ -465,10 +399,10 @@ test('Accessibility of role management page', async ({
|
|||||||
await adminEditUserPage.roleInput.click();
|
await adminEditUserPage.roleInput.click();
|
||||||
await adminEditUserPage.page.getByRole('option', { name: 'Admin' }).click();
|
await adminEditUserPage.page.getByRole('option', { name: 'Admin' }).click();
|
||||||
await adminEditUserPage.updateButton.click();
|
await adminEditUserPage.updateButton.click();
|
||||||
await adminEditUserPage.snackbar.waitFor({
|
const snackbar = await adminEditUserPage.getSnackbarData(
|
||||||
state: 'attached',
|
'snackbar-edit-user-success'
|
||||||
});
|
);
|
||||||
await adminEditUserPage.closeSnackbar();
|
await expect(snackbar.variant).toBe('success');
|
||||||
});
|
});
|
||||||
|
|
||||||
await test.step('Delete the role', async () => {
|
await test.step('Delete the role', async () => {
|
||||||
@@ -480,14 +414,10 @@ test('Accessibility of role management page', async ({
|
|||||||
state: 'attached',
|
state: 'attached',
|
||||||
});
|
});
|
||||||
await deleteModal.deleteButton.click();
|
await deleteModal.deleteButton.click();
|
||||||
await adminRolesPage.snackbar.waitFor({
|
|
||||||
state: 'attached',
|
|
||||||
});
|
|
||||||
const snackbar = await adminRolesPage.getSnackbarData(
|
const snackbar = await adminRolesPage.getSnackbarData(
|
||||||
'snackbar-delete-role-success'
|
'snackbar-delete-role-success'
|
||||||
);
|
);
|
||||||
await expect(snackbar.variant).toBe('success');
|
await expect(snackbar.variant).toBe('success');
|
||||||
await adminRolesPage.closeSnackbar();
|
|
||||||
await deleteModal.modal.waitFor({
|
await deleteModal.modal.waitFor({
|
||||||
state: 'detached',
|
state: 'detached',
|
||||||
});
|
});
|
||||||
|
@@ -5,281 +5,235 @@ const { test, expect } = require('../../fixtures/index');
|
|||||||
* otherwise tests will fail since users are only *soft*-deleted
|
* otherwise tests will fail since users are only *soft*-deleted
|
||||||
*/
|
*/
|
||||||
test.describe('User management page', () => {
|
test.describe('User management page', () => {
|
||||||
|
|
||||||
test.beforeEach(async ({ adminUsersPage }) => {
|
test.beforeEach(async ({ adminUsersPage }) => {
|
||||||
await adminUsersPage.navigateTo();
|
await adminUsersPage.navigateTo();
|
||||||
await adminUsersPage.closeSnackbar();
|
await adminUsersPage.closeAllSnackbars();
|
||||||
});
|
});
|
||||||
|
|
||||||
test(
|
test('User creation and deletion process', async ({
|
||||||
'User creation and deletion process',
|
adminCreateUserPage,
|
||||||
async ({ adminCreateUserPage, adminEditUserPage, adminUsersPage }) => {
|
adminEditUserPage,
|
||||||
adminCreateUserPage.seed(9000);
|
adminUsersPage,
|
||||||
const user = adminCreateUserPage.generateUser();
|
}) => {
|
||||||
await adminUsersPage.usersLoader.waitFor({
|
adminCreateUserPage.seed(9000);
|
||||||
state: 'detached' /* Note: state: 'visible' introduces flakiness
|
const user = adminCreateUserPage.generateUser();
|
||||||
|
await adminUsersPage.usersLoader.waitFor({
|
||||||
|
state: 'detached' /* Note: state: 'visible' introduces flakiness
|
||||||
because visibility: hidden is used as part of the state transition in
|
because visibility: hidden is used as part of the state transition in
|
||||||
notistack, see
|
notistack, see
|
||||||
https://github.com/iamhosseindhv/notistack/blob/122f47057eb7ce5a1abfe923316cf8475303e99a/src/transitions/Collapse/Collapse.tsx#L110
|
https://github.com/iamhosseindhv/notistack/blob/122f47057eb7ce5a1abfe923316cf8475303e99a/src/transitions/Collapse/Collapse.tsx#L110
|
||||||
*/
|
*/,
|
||||||
|
});
|
||||||
|
await test.step('Create a user', async () => {
|
||||||
|
await adminUsersPage.createUserButton.click();
|
||||||
|
await adminCreateUserPage.fullNameInput.fill(user.fullName);
|
||||||
|
await adminCreateUserPage.emailInput.fill(user.email);
|
||||||
|
await adminCreateUserPage.roleInput.click();
|
||||||
|
await adminCreateUserPage.page
|
||||||
|
.getByRole('option', { name: 'Admin' })
|
||||||
|
.click();
|
||||||
|
await adminCreateUserPage.createButton.click();
|
||||||
|
await adminCreateUserPage.invitationEmailInfoAlert.waitFor({
|
||||||
|
state: 'attached',
|
||||||
});
|
});
|
||||||
await test.step(
|
|
||||||
'Create a user',
|
|
||||||
async () => {
|
|
||||||
await adminUsersPage.createUserButton.click();
|
|
||||||
await adminCreateUserPage.fullNameInput.fill(user.fullName);
|
|
||||||
await adminCreateUserPage.emailInput.fill(user.email);
|
|
||||||
await adminCreateUserPage.roleInput.click();
|
|
||||||
await adminCreateUserPage.page.getByRole(
|
|
||||||
'option', { name: 'Admin' }
|
|
||||||
).click();
|
|
||||||
await adminCreateUserPage.createButton.click();
|
|
||||||
await adminCreateUserPage.invitationEmailInfoAlert.waitFor({
|
|
||||||
state: 'attached'
|
|
||||||
});
|
|
||||||
|
|
||||||
const snackbar = await adminUsersPage.getSnackbarData(
|
const snackbar = await adminUsersPage.getSnackbarData(
|
||||||
'snackbar-create-user-success'
|
'snackbar-create-user-success'
|
||||||
);
|
|
||||||
await expect(snackbar.variant).toBe('success');
|
|
||||||
await adminUsersPage.navigateTo();
|
|
||||||
await adminUsersPage.closeSnackbar();
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
await test.step(
|
await expect(snackbar.variant).toBe('success');
|
||||||
'Check the user exists with the expected properties',
|
await adminUsersPage.navigateTo();
|
||||||
async () => {
|
});
|
||||||
await adminUsersPage.findUserPageWithEmail(user.email);
|
await test.step('Check the user exists with the expected properties', async () => {
|
||||||
const userRow = await adminUsersPage.getUserRowByEmail(user.email);
|
await adminUsersPage.findUserPageWithEmail(user.email);
|
||||||
const data = await adminUsersPage.getRowData(userRow);
|
const userRow = await adminUsersPage.getUserRowByEmail(user.email);
|
||||||
await expect(data.email).toBe(user.email);
|
const data = await adminUsersPage.getRowData(userRow);
|
||||||
await expect(data.fullName).toBe(user.fullName);
|
await expect(data.email).toBe(user.email);
|
||||||
await expect(data.role).toBe('Admin');
|
await expect(data.fullName).toBe(user.fullName);
|
||||||
}
|
await expect(data.role).toBe('Admin');
|
||||||
|
});
|
||||||
|
await test.step('Edit user info and make sure the edit works correctly', async () => {
|
||||||
|
await adminUsersPage.findUserPageWithEmail(user.email);
|
||||||
|
|
||||||
|
let userRow = await adminUsersPage.getUserRowByEmail(user.email);
|
||||||
|
await adminUsersPage.clickEditUser(userRow);
|
||||||
|
await adminEditUserPage.waitForLoad(user.fullName);
|
||||||
|
const newUserInfo = adminEditUserPage.generateUser();
|
||||||
|
await adminEditUserPage.fullNameInput.fill(newUserInfo.fullName);
|
||||||
|
await adminEditUserPage.updateButton.click();
|
||||||
|
|
||||||
|
const snackbar = await adminUsersPage.getSnackbarData(
|
||||||
|
'snackbar-edit-user-success'
|
||||||
);
|
);
|
||||||
await test.step(
|
await expect(snackbar.variant).toBe('success');
|
||||||
'Edit user info and make sure the edit works correctly',
|
|
||||||
async () => {
|
|
||||||
await adminUsersPage.findUserPageWithEmail(user.email);
|
|
||||||
|
|
||||||
let userRow = await adminUsersPage.getUserRowByEmail(user.email);
|
await adminUsersPage.findUserPageWithEmail(user.email);
|
||||||
await adminUsersPage.clickEditUser(userRow);
|
userRow = await adminUsersPage.getUserRowByEmail(user.email);
|
||||||
await adminEditUserPage.waitForLoad(user.fullName);
|
const rowData = await adminUsersPage.getRowData(userRow);
|
||||||
const newUserInfo = adminEditUserPage.generateUser();
|
await expect(rowData.fullName).toBe(newUserInfo.fullName);
|
||||||
await adminEditUserPage.fullNameInput.fill(newUserInfo.fullName);
|
});
|
||||||
await adminEditUserPage.updateButton.click();
|
await test.step('Delete user and check the page confirms this deletion', async () => {
|
||||||
|
await adminUsersPage.findUserPageWithEmail(user.email);
|
||||||
|
const userRow = await adminUsersPage.getUserRowByEmail(user.email);
|
||||||
|
await adminUsersPage.clickDeleteUser(userRow);
|
||||||
|
const modal = adminUsersPage.deleteUserModal;
|
||||||
|
await modal.deleteButton.click();
|
||||||
|
|
||||||
const snackbar = await adminUsersPage.getSnackbarData(
|
const snackbar = await adminUsersPage.getSnackbarData(
|
||||||
'snackbar-edit-user-success'
|
'snackbar-delete-user-success'
|
||||||
);
|
|
||||||
await expect(snackbar.variant).toBe('success');
|
|
||||||
await adminUsersPage.closeSnackbar();
|
|
||||||
|
|
||||||
await adminUsersPage.findUserPageWithEmail(user.email);
|
|
||||||
userRow = await adminUsersPage.getUserRowByEmail(user.email);
|
|
||||||
const rowData = await adminUsersPage.getRowData(userRow);
|
|
||||||
await expect(rowData.fullName).toBe(newUserInfo.fullName);
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
await test.step(
|
await expect(snackbar.variant).toBe('success');
|
||||||
'Delete user and check the page confirms this deletion',
|
await expect(userRow).not.toBeVisible(false);
|
||||||
async () => {
|
});
|
||||||
await adminUsersPage.findUserPageWithEmail(user.email);
|
});
|
||||||
const userRow = await adminUsersPage.getUserRowByEmail(user.email);
|
|
||||||
await adminUsersPage.clickDeleteUser(userRow);
|
|
||||||
const modal = adminUsersPage.deleteUserModal;
|
|
||||||
await modal.deleteButton.click();
|
|
||||||
|
|
||||||
const snackbar = await adminUsersPage.getSnackbarData(
|
test('Creating a user which has been deleted', async ({
|
||||||
'snackbar-delete-user-success'
|
adminCreateUserPage,
|
||||||
);
|
adminUsersPage,
|
||||||
await expect(snackbar.variant).toBe('success');
|
}) => {
|
||||||
await adminUsersPage.closeSnackbar();
|
adminCreateUserPage.seed(9100);
|
||||||
await expect(userRow).not.toBeVisible(false);
|
const testUser = adminCreateUserPage.generateUser();
|
||||||
}
|
|
||||||
|
await test.step('Create the test user', async () => {
|
||||||
|
await adminUsersPage.navigateTo();
|
||||||
|
await adminUsersPage.createUserButton.click();
|
||||||
|
await adminCreateUserPage.fullNameInput.fill(testUser.fullName);
|
||||||
|
await adminCreateUserPage.emailInput.fill(testUser.email);
|
||||||
|
await adminCreateUserPage.roleInput.click();
|
||||||
|
await adminCreateUserPage.page
|
||||||
|
.getByRole('option', { name: 'Admin' })
|
||||||
|
.click();
|
||||||
|
await adminCreateUserPage.createButton.click();
|
||||||
|
const snackbar = await adminUsersPage.getSnackbarData(
|
||||||
|
'snackbar-create-user-success'
|
||||||
);
|
);
|
||||||
|
await expect(snackbar.variant).toBe('success');
|
||||||
});
|
});
|
||||||
|
|
||||||
test(
|
await test.step('Delete the created user', async () => {
|
||||||
'Creating a user which has been deleted',
|
await adminUsersPage.navigateTo();
|
||||||
async ({ adminCreateUserPage, adminUsersPage }) => {
|
await adminUsersPage.findUserPageWithEmail(testUser.email);
|
||||||
adminCreateUserPage.seed(9100);
|
const userRow = await adminUsersPage.getUserRowByEmail(testUser.email);
|
||||||
const testUser = adminCreateUserPage.generateUser();
|
await adminUsersPage.clickDeleteUser(userRow);
|
||||||
|
const modal = adminUsersPage.deleteUserModal;
|
||||||
await test.step(
|
await modal.deleteButton.click();
|
||||||
'Create the test user',
|
const snackbar = await adminUsersPage.getSnackbarData(
|
||||||
async () => {
|
'snackbar-delete-user-success'
|
||||||
await adminUsersPage.navigateTo();
|
|
||||||
await adminUsersPage.createUserButton.click();
|
|
||||||
await adminCreateUserPage.fullNameInput.fill(testUser.fullName);
|
|
||||||
await adminCreateUserPage.emailInput.fill(testUser.email);
|
|
||||||
await adminCreateUserPage.roleInput.click();
|
|
||||||
await adminCreateUserPage.page.getByRole(
|
|
||||||
'option', { name: 'Admin' }
|
|
||||||
).click();
|
|
||||||
await adminCreateUserPage.createButton.click();
|
|
||||||
const snackbar = await adminUsersPage.getSnackbarData(
|
|
||||||
'snackbar-create-user-success'
|
|
||||||
);
|
|
||||||
await expect(snackbar.variant).toBe('success');
|
|
||||||
await adminUsersPage.closeSnackbar();
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
await expect(snackbar).not.toBeNull();
|
||||||
|
await expect(snackbar.variant).toBe('success');
|
||||||
|
await expect(userRow).not.toBeVisible(false);
|
||||||
|
});
|
||||||
|
|
||||||
await test.step(
|
await test.step('Create the user again', async () => {
|
||||||
'Delete the created user',
|
await adminUsersPage.createUserButton.click();
|
||||||
async () => {
|
await adminCreateUserPage.fullNameInput.fill(testUser.fullName);
|
||||||
await adminUsersPage.navigateTo();
|
await adminCreateUserPage.emailInput.fill(testUser.email);
|
||||||
await adminUsersPage.findUserPageWithEmail(testUser.email);
|
await adminCreateUserPage.roleInput.click();
|
||||||
const userRow = await adminUsersPage.getUserRowByEmail(testUser.email);
|
await adminCreateUserPage.page
|
||||||
await adminUsersPage.clickDeleteUser(userRow);
|
.getByRole('option', { name: 'Admin' })
|
||||||
const modal = adminUsersPage.deleteUserModal;
|
.click();
|
||||||
await modal.deleteButton.click();
|
await adminCreateUserPage.createButton.click();
|
||||||
const snackbar = await adminUsersPage.getSnackbarData(
|
const snackbar = await adminUsersPage.getSnackbarData('snackbar-error');
|
||||||
'snackbar-delete-user-success'
|
await expect(snackbar.variant).toBe('error');
|
||||||
);
|
});
|
||||||
await expect(snackbar).not.toBeNull();
|
});
|
||||||
await expect(snackbar.variant).toBe('success');
|
|
||||||
await adminUsersPage.closeSnackbar();
|
test('Creating a user which already exists', async ({
|
||||||
await expect(userRow).not.toBeVisible(false);
|
adminCreateUserPage,
|
||||||
}
|
adminUsersPage,
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
adminCreateUserPage.seed(9200);
|
||||||
|
const testUser = adminCreateUserPage.generateUser();
|
||||||
|
|
||||||
|
await test.step('Create the test user', async () => {
|
||||||
|
await adminUsersPage.createUserButton.click();
|
||||||
|
await adminCreateUserPage.fullNameInput.fill(testUser.fullName);
|
||||||
|
await adminCreateUserPage.emailInput.fill(testUser.email);
|
||||||
|
await adminCreateUserPage.roleInput.click();
|
||||||
|
await adminCreateUserPage.page
|
||||||
|
.getByRole('option', { name: 'Admin' })
|
||||||
|
.click();
|
||||||
|
await adminCreateUserPage.createButton.click();
|
||||||
|
const snackbar = await adminUsersPage.getSnackbarData(
|
||||||
|
'snackbar-create-user-success'
|
||||||
);
|
);
|
||||||
|
await expect(snackbar.variant).toBe('success');
|
||||||
|
});
|
||||||
|
|
||||||
await test.step(
|
await test.step('Create the user again', async () => {
|
||||||
'Create the user again',
|
await adminUsersPage.navigateTo();
|
||||||
async () => {
|
await adminUsersPage.createUserButton.click();
|
||||||
await adminUsersPage.createUserButton.click();
|
await adminCreateUserPage.fullNameInput.fill(testUser.fullName);
|
||||||
await adminCreateUserPage.fullNameInput.fill(testUser.fullName);
|
await adminCreateUserPage.emailInput.fill(testUser.email);
|
||||||
await adminCreateUserPage.emailInput.fill(testUser.email);
|
const createUserPageUrl = page.url();
|
||||||
await adminCreateUserPage.roleInput.click();
|
await adminCreateUserPage.roleInput.click();
|
||||||
await adminCreateUserPage.page.getByRole(
|
await adminCreateUserPage.page
|
||||||
'option', { name: 'Admin' }
|
.getByRole('option', { name: 'Admin' })
|
||||||
).click();
|
.click();
|
||||||
await adminCreateUserPage.createButton.click();
|
await adminCreateUserPage.createButton.click();
|
||||||
const snackbar = await adminUsersPage.getSnackbarData('snackbar-error');
|
|
||||||
await expect(snackbar.variant).toBe('error');
|
await expect(page.url()).toBe(createUserPageUrl);
|
||||||
await adminUsersPage.closeSnackbar();
|
const snackbar = await adminUsersPage.getSnackbarData('snackbar-error');
|
||||||
}
|
await expect(snackbar.variant).toBe('error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Editing a user to have the same email as another user should not be allowed', async ({
|
||||||
|
adminCreateUserPage,
|
||||||
|
adminEditUserPage,
|
||||||
|
adminUsersPage,
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
adminCreateUserPage.seed(9300);
|
||||||
|
const user1 = adminCreateUserPage.generateUser();
|
||||||
|
const user2 = adminCreateUserPage.generateUser();
|
||||||
|
await test.step('Create the first user', async () => {
|
||||||
|
await adminUsersPage.navigateTo();
|
||||||
|
await adminUsersPage.createUserButton.click();
|
||||||
|
await adminCreateUserPage.fullNameInput.fill(user1.fullName);
|
||||||
|
await adminCreateUserPage.emailInput.fill(user1.email);
|
||||||
|
await adminCreateUserPage.roleInput.click();
|
||||||
|
await adminCreateUserPage.page
|
||||||
|
.getByRole('option', { name: 'Admin' })
|
||||||
|
.click();
|
||||||
|
await adminCreateUserPage.createButton.click();
|
||||||
|
const snackbar = await adminUsersPage.getSnackbarData(
|
||||||
|
'snackbar-create-user-success'
|
||||||
);
|
);
|
||||||
}
|
await expect(snackbar.variant).toBe('success');
|
||||||
);
|
await adminUsersPage.closeAllSnackbars();
|
||||||
|
});
|
||||||
|
|
||||||
test(
|
await test.step('Create the second user', async () => {
|
||||||
'Creating a user which already exists',
|
await adminUsersPage.navigateTo();
|
||||||
async ({ adminCreateUserPage, adminUsersPage, page }) => {
|
await adminUsersPage.createUserButton.click();
|
||||||
adminCreateUserPage.seed(9200);
|
await adminCreateUserPage.fullNameInput.fill(user2.fullName);
|
||||||
const testUser = adminCreateUserPage.generateUser();
|
await adminCreateUserPage.emailInput.fill(user2.email);
|
||||||
|
await adminCreateUserPage.roleInput.click();
|
||||||
await test.step(
|
await adminCreateUserPage.page
|
||||||
'Create the test user',
|
.getByRole('option', { name: 'Admin' })
|
||||||
async () => {
|
.click();
|
||||||
await adminUsersPage.createUserButton.click();
|
await adminCreateUserPage.createButton.click();
|
||||||
await adminCreateUserPage.fullNameInput.fill(testUser.fullName);
|
const snackbar = await adminUsersPage.getSnackbarData(
|
||||||
await adminCreateUserPage.emailInput.fill(testUser.email);
|
'snackbar-create-user-success'
|
||||||
await adminCreateUserPage.roleInput.click();
|
|
||||||
await adminCreateUserPage.page.getByRole(
|
|
||||||
'option', { name: 'Admin' }
|
|
||||||
).click();
|
|
||||||
await adminCreateUserPage.createButton.click();
|
|
||||||
const snackbar = await adminUsersPage.getSnackbarData(
|
|
||||||
'snackbar-create-user-success'
|
|
||||||
);
|
|
||||||
await expect(snackbar.variant).toBe('success');
|
|
||||||
await adminUsersPage.closeSnackbar();
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
await expect(snackbar.variant).toBe('success');
|
||||||
|
});
|
||||||
|
|
||||||
await test.step(
|
await test.step('Try editing the second user to have the email of the first user', async () => {
|
||||||
'Create the user again',
|
await adminUsersPage.navigateTo();
|
||||||
async () => {
|
await adminUsersPage.findUserPageWithEmail(user2.email);
|
||||||
await adminUsersPage.navigateTo();
|
let userRow = await adminUsersPage.getUserRowByEmail(user2.email);
|
||||||
await adminUsersPage.createUserButton.click();
|
await adminUsersPage.clickEditUser(userRow);
|
||||||
await adminCreateUserPage.fullNameInput.fill(testUser.fullName);
|
await adminEditUserPage.waitForLoad(user2.fullName);
|
||||||
await adminCreateUserPage.emailInput.fill(testUser.email);
|
await adminEditUserPage.emailInput.fill(user1.email);
|
||||||
const createUserPageUrl = page.url();
|
const editPageUrl = page.url();
|
||||||
await adminCreateUserPage.roleInput.click();
|
await adminEditUserPage.updateButton.click();
|
||||||
await adminCreateUserPage.page.getByRole(
|
|
||||||
'option', { name: 'Admin' }
|
|
||||||
).click();
|
|
||||||
await adminCreateUserPage.createButton.click();
|
|
||||||
|
|
||||||
await expect(page.url()).toBe(createUserPageUrl);
|
const snackbar = await adminUsersPage.getSnackbarData('snackbar-error');
|
||||||
const snackbar = await adminUsersPage.getSnackbarData('snackbar-error');
|
await expect(snackbar.variant).toBe('error');
|
||||||
await expect(snackbar.variant).toBe('error');
|
await expect(page.url()).toBe(editPageUrl);
|
||||||
await adminUsersPage.closeSnackbar();
|
});
|
||||||
}
|
});
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
test(
|
|
||||||
'Editing a user to have the same email as another user should not be allowed',
|
|
||||||
async ({
|
|
||||||
adminCreateUserPage, adminEditUserPage, adminUsersPage, page
|
|
||||||
}) => {
|
|
||||||
adminCreateUserPage.seed(9300);
|
|
||||||
const user1 = adminCreateUserPage.generateUser();
|
|
||||||
const user2 = adminCreateUserPage.generateUser();
|
|
||||||
await test.step(
|
|
||||||
'Create the first user',
|
|
||||||
async () => {
|
|
||||||
await adminUsersPage.navigateTo();
|
|
||||||
await adminUsersPage.createUserButton.click();
|
|
||||||
await adminCreateUserPage.fullNameInput.fill(user1.fullName);
|
|
||||||
await adminCreateUserPage.emailInput.fill(user1.email);
|
|
||||||
await adminCreateUserPage.roleInput.click();
|
|
||||||
await adminCreateUserPage.page.getByRole(
|
|
||||||
'option', { name: 'Admin' }
|
|
||||||
).click();
|
|
||||||
await adminCreateUserPage.createButton.click();
|
|
||||||
const snackbar = await adminUsersPage.getSnackbarData(
|
|
||||||
'snackbar-create-user-success'
|
|
||||||
);
|
|
||||||
await expect(snackbar.variant).toBe('success');
|
|
||||||
await adminUsersPage.closeSnackbar();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
await test.step(
|
|
||||||
'Create the second user',
|
|
||||||
async () => {
|
|
||||||
await adminUsersPage.navigateTo();
|
|
||||||
await adminUsersPage.createUserButton.click();
|
|
||||||
await adminCreateUserPage.fullNameInput.fill(user2.fullName);
|
|
||||||
await adminCreateUserPage.emailInput.fill(user2.email);
|
|
||||||
await adminCreateUserPage.roleInput.click();
|
|
||||||
await adminCreateUserPage.page.getByRole(
|
|
||||||
'option', { name: 'Admin' }
|
|
||||||
).click();
|
|
||||||
await adminCreateUserPage.createButton.click();
|
|
||||||
const snackbar = await adminUsersPage.getSnackbarData(
|
|
||||||
'snackbar-create-user-success'
|
|
||||||
);
|
|
||||||
await expect(snackbar.variant).toBe('success');
|
|
||||||
await adminUsersPage.closeSnackbar();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
await test.step(
|
|
||||||
'Try editing the second user to have the email of the first user',
|
|
||||||
async () => {
|
|
||||||
await adminUsersPage.navigateTo();
|
|
||||||
await adminUsersPage.findUserPageWithEmail(user2.email);
|
|
||||||
let userRow = await adminUsersPage.getUserRowByEmail(user2.email);
|
|
||||||
await adminUsersPage.clickEditUser(userRow);
|
|
||||||
await adminEditUserPage.waitForLoad(user2.fullName);
|
|
||||||
await adminEditUserPage.emailInput.fill(user1.email);
|
|
||||||
const editPageUrl = page.url();
|
|
||||||
await adminEditUserPage.updateButton.click();
|
|
||||||
|
|
||||||
const snackbar = await adminUsersPage.getSnackbarData(
|
|
||||||
'snackbar-error'
|
|
||||||
);
|
|
||||||
await expect(snackbar.variant).toBe('error');
|
|
||||||
await adminUsersPage.closeSnackbar();
|
|
||||||
await expect(page.url()).toBe(editPageUrl);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
@@ -1,24 +1,55 @@
|
|||||||
|
const { request } = require('@playwright/test');
|
||||||
const { test, expect } = require('../../fixtures/index');
|
const { test, expect } = require('../../fixtures/index');
|
||||||
const {AddMattermostConnectionModal} = require('../../fixtures/apps/mattermost/add-mattermost-connection-modal');
|
const {
|
||||||
|
AddMattermostConnectionModal,
|
||||||
|
} = require('../../fixtures/apps/mattermost/add-mattermost-connection-modal');
|
||||||
|
const { createFlow, updateFlowName, getFlow, updateFlowStep, testStep } = require('../../helpers/flow-api-helper');
|
||||||
|
const { getToken } = require('../../helpers/auth-api-helper');
|
||||||
|
|
||||||
test.describe('Pop-up message on connections', () => {
|
test.describe('Pop-up message on connections', () => {
|
||||||
test.beforeEach(async ({ flowEditorPage, page }) => {
|
test.beforeEach(async ({ flowEditorPage, page }) => {
|
||||||
await page.getByTestId('create-flow-button').click();
|
const apiRequest = await request.newContext();
|
||||||
await page.waitForURL(
|
const tokenJsonResponse = await getToken(apiRequest);
|
||||||
/\/editor\/[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}/
|
const token = tokenJsonResponse.data.token;
|
||||||
);
|
|
||||||
await expect(page.getByTestId('flow-step')).toHaveCount(2);
|
|
||||||
|
|
||||||
await flowEditorPage.flowName.click();
|
let flow = await createFlow(apiRequest, token);
|
||||||
await flowEditorPage.flowNameInput.fill('PopupFlow');
|
const flowId = flow.data.id;
|
||||||
await flowEditorPage.createWebhookTrigger(true);
|
await updateFlowName(apiRequest, token, flowId);
|
||||||
|
flow = await getFlow(apiRequest, token, flowId);
|
||||||
|
const flowSteps = flow.data.steps;
|
||||||
|
|
||||||
await flowEditorPage.chooseAppAndEvent('Mattermost', 'Send a message to channel');
|
const triggerStepId = flowSteps.find((step) => step.type === 'trigger').id;
|
||||||
await expect(flowEditorPage.continueButton).toHaveCount(1);
|
const actionStepId = flowSteps.find((step) => step.type === 'action').id;
|
||||||
await expect(flowEditorPage.continueButton).not.toBeEnabled();
|
|
||||||
|
const triggerStep = await updateFlowStep(apiRequest, token, triggerStepId, {
|
||||||
|
appKey: 'webhook',
|
||||||
|
key: 'catchRawWebhook',
|
||||||
|
parameters: {
|
||||||
|
workSynchronously: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await apiRequest.get(triggerStep.data.webhookUrl);
|
||||||
|
await testStep(apiRequest, token, triggerStepId);
|
||||||
|
|
||||||
|
await updateFlowStep(apiRequest, token, actionStepId, {
|
||||||
|
appKey: 'mattermost',
|
||||||
|
key: 'sendMessageToChannel',
|
||||||
|
});
|
||||||
|
await testStep(apiRequest, token, actionStepId);
|
||||||
|
|
||||||
|
await page.reload();
|
||||||
|
|
||||||
|
const flowRow = await page.getByTestId('flow-row').filter({
|
||||||
|
hasText: flowId,
|
||||||
|
});
|
||||||
|
await flowRow.click();
|
||||||
|
const flowTriggerStep = await page.getByTestId('flow-step').nth(1);
|
||||||
|
await flowTriggerStep.click();
|
||||||
|
await page.getByText('Choose connection').click();
|
||||||
|
|
||||||
await flowEditorPage.connectionAutocomplete.click();
|
await flowEditorPage.connectionAutocomplete.click();
|
||||||
await flowEditorPage.addNewConnectionItem.click(); });
|
await flowEditorPage.addNewConnectionItem.click();
|
||||||
|
});
|
||||||
|
|
||||||
test('should show error to remind to enable pop-up on connection create', async ({
|
test('should show error to remind to enable pop-up on connection create', async ({
|
||||||
page,
|
page,
|
||||||
@@ -28,7 +59,7 @@ test.describe('Pop-up message on connections', () => {
|
|||||||
// Inject script to override window.open
|
// Inject script to override window.open
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
window.open = function() {
|
window.open = function () {
|
||||||
console.log('Popup blocked!');
|
console.log('Popup blocked!');
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
@@ -37,8 +68,10 @@ test.describe('Pop-up message on connections', () => {
|
|||||||
await addMattermostConnectionModal.fillConnectionForm();
|
await addMattermostConnectionModal.fillConnectionForm();
|
||||||
await addMattermostConnectionModal.submitConnectionForm();
|
await addMattermostConnectionModal.submitConnectionForm();
|
||||||
|
|
||||||
await expect(page.getByTestId("add-connection-error")).toHaveCount(1);
|
await expect(page.getByTestId('add-connection-error')).toHaveCount(1);
|
||||||
await expect(page.getByTestId("add-connection-error")).toHaveText('Make sure pop-ups are enabled in your browser.');
|
await expect(page.getByTestId('add-connection-error')).toHaveText(
|
||||||
|
'Make sure pop-ups are enabled in your browser.'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not show pop-up error if pop-ups are enabled on connection create', async ({
|
test('should not show pop-up error if pop-ups are enabled on connection create', async ({
|
||||||
@@ -51,13 +84,15 @@ test.describe('Pop-up message on connections', () => {
|
|||||||
await addMattermostConnectionModal.submitConnectionForm();
|
await addMattermostConnectionModal.submitConnectionForm();
|
||||||
|
|
||||||
const popup = await popupPromise;
|
const popup = await popupPromise;
|
||||||
await expect(popup.url()).toContain("mattermost");
|
await expect(popup.url()).toContain('mattermost');
|
||||||
await expect(page.getByTestId("add-connection-error")).toHaveCount(0);
|
await expect(page.getByTestId('add-connection-error')).toHaveCount(0);
|
||||||
|
|
||||||
await test.step('Should show error on failed credentials verification', async () => {
|
await test.step('Should show error on failed credentials verification', async () => {
|
||||||
await popup.close();
|
await popup.close();
|
||||||
await expect(page.getByTestId("add-connection-error")).toHaveCount(1);
|
await expect(page.getByTestId('add-connection-error')).toHaveCount(1);
|
||||||
await expect(page.getByTestId("add-connection-error")).toHaveText('Error occured while verifying credentials!');
|
await expect(page.getByTestId('add-connection-error')).toHaveText(
|
||||||
|
'Error occured while verifying credentials!'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
@@ -1,57 +1,38 @@
|
|||||||
|
const { request } = require('@playwright/test');
|
||||||
const { publicTest, expect } = require('../../fixtures/index');
|
const { publicTest, expect } = require('../../fixtures/index');
|
||||||
const { AdminUsersPage } = require('../../fixtures/admin/users-page');
|
|
||||||
const { MyProfilePage } = require('../../fixtures/my-profile-page');
|
const { MyProfilePage } = require('../../fixtures/my-profile-page');
|
||||||
const { LoginPage } = require('../../fixtures/login-page');
|
const { LoginPage } = require('../../fixtures/login-page');
|
||||||
|
const { addUser, acceptInvitation } = require('../../helpers/user-api-helper');
|
||||||
|
const { getToken } = require('../../helpers/auth-api-helper');
|
||||||
|
|
||||||
publicTest.describe('My Profile', () => {
|
publicTest.describe('My Profile', () => {
|
||||||
let testUser;
|
let testUser;
|
||||||
|
|
||||||
publicTest.beforeEach(
|
publicTest.beforeEach(
|
||||||
async ({ acceptInvitationPage, adminCreateUserPage, loginPage, page }) => {
|
async ({ adminCreateUserPage, loginPage, page }) => {
|
||||||
let acceptInvitationLink;
|
let addUserResponse;
|
||||||
|
const apiRequest = await request.newContext();
|
||||||
|
|
||||||
adminCreateUserPage.seed(
|
adminCreateUserPage.seed(
|
||||||
Math.ceil(Math.random() * Number.MAX_SAFE_INTEGER)
|
Math.ceil(Math.random() * Number.MAX_SAFE_INTEGER)
|
||||||
);
|
);
|
||||||
testUser = adminCreateUserPage.generateUser();
|
testUser = adminCreateUserPage.generateUser();
|
||||||
|
|
||||||
const adminUsersPage = new AdminUsersPage(page);
|
|
||||||
const myProfilePage = new MyProfilePage(page);
|
|
||||||
|
|
||||||
await publicTest.step('login as Admin', async () => {
|
|
||||||
await loginPage.login();
|
|
||||||
await expect(loginPage.page).toHaveURL('/flows');
|
|
||||||
});
|
|
||||||
|
|
||||||
await publicTest.step('create new user', async () => {
|
await publicTest.step('create new user', async () => {
|
||||||
await adminUsersPage.navigateTo();
|
const tokenJsonResponse = await getToken(apiRequest);
|
||||||
await adminUsersPage.createUserButton.click();
|
addUserResponse = await addUser(
|
||||||
await adminCreateUserPage.fullNameInput.fill(testUser.fullName);
|
apiRequest,
|
||||||
await adminCreateUserPage.emailInput.fill(testUser.email);
|
tokenJsonResponse.data.token,
|
||||||
await adminCreateUserPage.roleInput.click();
|
{
|
||||||
await adminCreateUserPage.page
|
fullName: testUser.fullName,
|
||||||
.getByRole('option', { name: 'Admin' })
|
email: testUser.email,
|
||||||
.click();
|
}
|
||||||
await adminCreateUserPage.createButton.click();
|
|
||||||
const snackbar = await adminUsersPage.getSnackbarData(
|
|
||||||
'snackbar-create-user-success'
|
|
||||||
);
|
);
|
||||||
await expect(snackbar.variant).toBe('success');
|
|
||||||
});
|
|
||||||
|
|
||||||
await publicTest.step('copy invitation link', async () => {
|
|
||||||
const invitationMessage =
|
|
||||||
await adminCreateUserPage.acceptInvitationLink;
|
|
||||||
acceptInvitationLink = await invitationMessage.getAttribute('href');
|
|
||||||
});
|
|
||||||
|
|
||||||
await publicTest.step('logout', async () => {
|
|
||||||
await myProfilePage.logout();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await publicTest.step('accept invitation', async () => {
|
await publicTest.step('accept invitation', async () => {
|
||||||
await page.goto(acceptInvitationLink);
|
let acceptToken = addUserResponse.data.acceptInvitationUrl.split('=')[1];
|
||||||
await acceptInvitationPage.acceptInvitation(LoginPage.defaultPassword);
|
await acceptInvitation(apiRequest, {token:acceptToken, password:LoginPage.defaultPassword});
|
||||||
});
|
});
|
||||||
|
|
||||||
await publicTest.step('login as new Admin', async () => {
|
await publicTest.step('login as new Admin', async () => {
|
||||||
|
1145
packages/e2e-tests/yarn.lock
Normal file
1145
packages/e2e-tests/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,7 @@
|
|||||||
"@testing-library/jest-dom": "^5.11.4",
|
"@testing-library/jest-dom": "^5.11.4",
|
||||||
"@testing-library/react": "^11.1.0",
|
"@testing-library/react": "^11.1.0",
|
||||||
"@testing-library/user-event": "^12.1.10",
|
"@testing-library/user-event": "^12.1.10",
|
||||||
|
"axios": "^1.6.0",
|
||||||
"clipboard-copy": "^4.0.1",
|
"clipboard-copy": "^4.0.1",
|
||||||
"compare-versions": "^4.1.3",
|
"compare-versions": "^4.1.3",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
@@ -112,7 +112,7 @@ export default function ResetPasswordForm() {
|
|||||||
<Alert
|
<Alert
|
||||||
data-test="accept-invitation-form-error"
|
data-test="accept-invitation-form-error"
|
||||||
severity="error"
|
severity="error"
|
||||||
sx={{ mt: 1, fontWeight: 500 }}
|
sx={{ mt: 1 }}
|
||||||
>
|
>
|
||||||
{formatMessage('acceptInvitationForm.invalidToken')}
|
{formatMessage('acceptInvitationForm.invalidToken')}
|
||||||
</Alert>
|
</Alert>
|
||||||
|
@@ -126,7 +126,7 @@ function AddAppConnection(props) {
|
|||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
|
|
||||||
{authDocUrl && (
|
{authDocUrl && (
|
||||||
<Alert severity="info" sx={{ fontWeight: 300 }}>
|
<Alert severity="info">
|
||||||
{formatMessage('addAppConnection.callToDocs', {
|
{formatMessage('addAppConnection.callToDocs', {
|
||||||
appName: name,
|
appName: name,
|
||||||
docsLink: generateExternalLink(authDocUrl),
|
docsLink: generateExternalLink(authDocUrl),
|
||||||
@@ -138,7 +138,7 @@ function AddAppConnection(props) {
|
|||||||
<Alert
|
<Alert
|
||||||
data-test="add-connection-error"
|
data-test="add-connection-error"
|
||||||
severity="error"
|
severity="error"
|
||||||
sx={{ mt: 1, fontWeight: 500, wordBreak: 'break-all' }}
|
sx={{ mt: 1, wordBreak: 'break-all' }}
|
||||||
>
|
>
|
||||||
{!errorDetails && errorMessage}
|
{!errorDetails && errorMessage}
|
||||||
{errorDetails && (
|
{errorDetails && (
|
||||||
|
@@ -32,10 +32,7 @@ function AdminApplicationAuthClientDialog(props) {
|
|||||||
<Dialog open={true} onClose={onClose}>
|
<Dialog open={true} onClose={onClose}>
|
||||||
<DialogTitle>{title}</DialogTitle>
|
<DialogTitle>{title}</DialogTitle>
|
||||||
{error && (
|
{error && (
|
||||||
<Alert
|
<Alert severity="error" sx={{ mt: 1, wordBreak: 'break-all' }}>
|
||||||
severity="error"
|
|
||||||
sx={{ mt: 1, fontWeight: 500, wordBreak: 'break-all' }}
|
|
||||||
>
|
|
||||||
{error.message}
|
{error.message}
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
@@ -29,6 +29,8 @@ function ControlledAutocomplete(props) {
|
|||||||
options = [],
|
options = [],
|
||||||
dependsOn = [],
|
dependsOn = [],
|
||||||
showOptionValue,
|
showOptionValue,
|
||||||
|
renderInput,
|
||||||
|
showHelperText = true,
|
||||||
...autocompleteProps
|
...autocompleteProps
|
||||||
} = props;
|
} = props;
|
||||||
let dependsOnValues = [];
|
let dependsOnValues = [];
|
||||||
@@ -105,16 +107,18 @@ function ControlledAutocomplete(props) {
|
|||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
|
renderInput={(params) => renderInput(params, fieldState)}
|
||||||
/>
|
/>
|
||||||
|
{showHelperText && (
|
||||||
<FormHelperText
|
<FormHelperText
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
error={Boolean(fieldState.isTouched && fieldState.error)}
|
error={Boolean(fieldState.isTouched && fieldState.error)}
|
||||||
>
|
>
|
||||||
{fieldState.isTouched
|
{fieldState.isTouched
|
||||||
? fieldState.error?.message || description
|
? fieldState.error?.message || description
|
||||||
: description}
|
: description}
|
||||||
</FormHelperText>
|
</FormHelperText>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -132,6 +136,8 @@ ControlledAutocomplete.propTypes = {
|
|||||||
onBlur: PropTypes.func,
|
onBlur: PropTypes.func,
|
||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
options: PropTypes.array,
|
options: PropTypes.array,
|
||||||
|
renderInput: PropTypes.func.isRequired,
|
||||||
|
showHelperText: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ControlledAutocomplete;
|
export default ControlledAutocomplete;
|
||||||
|
@@ -6,7 +6,7 @@ import FormHelperText from '@mui/material/FormHelperText';
|
|||||||
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
||||||
import ClearIcon from '@mui/icons-material/Clear';
|
import ClearIcon from '@mui/icons-material/Clear';
|
||||||
import { ActionButtonsWrapper } from './style';
|
import { ActionButtonsWrapper } from './style';
|
||||||
import ClickAwayListener from '@mui/base/ClickAwayListener';
|
import { ClickAwayListener } from '@mui/base/ClickAwayListener';
|
||||||
import InputLabel from '@mui/material/InputLabel';
|
import InputLabel from '@mui/material/InputLabel';
|
||||||
import { createEditor } from 'slate';
|
import { createEditor } from 'slate';
|
||||||
import { Editable, ReactEditor } from 'slate-react';
|
import { Editable, ReactEditor } from 'slate-react';
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import Paper from '@mui/material/Paper';
|
import Paper from '@mui/material/Paper';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
|
import Alert from '@mui/material/Alert';
|
||||||
import LoadingButton from '@mui/lab/LoadingButton';
|
import LoadingButton from '@mui/lab/LoadingButton';
|
||||||
import { enqueueSnackbar } from 'notistack';
|
|
||||||
|
|
||||||
import useForgotPassword from 'hooks/useForgotPassword';
|
import useForgotPassword from 'hooks/useForgotPassword';
|
||||||
import Form from 'components/Form';
|
import Form from 'components/Form';
|
||||||
@@ -12,25 +12,17 @@ import useFormatMessage from 'hooks/useFormatMessage';
|
|||||||
export default function ForgotPasswordForm() {
|
export default function ForgotPasswordForm() {
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const {
|
const {
|
||||||
mutateAsync: forgotPassword,
|
mutate: forgotPassword,
|
||||||
isPending: loading,
|
isPending: loading,
|
||||||
isSuccess,
|
isSuccess,
|
||||||
|
isError,
|
||||||
|
error,
|
||||||
} = useForgotPassword();
|
} = useForgotPassword();
|
||||||
|
|
||||||
const handleSubmit = async (values) => {
|
const handleSubmit = ({ email }) => {
|
||||||
const { email } = values;
|
forgotPassword({
|
||||||
try {
|
email,
|
||||||
await forgotPassword({
|
});
|
||||||
email,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
enqueueSnackbar(
|
|
||||||
error?.message || formatMessage('forgotPasswordForm.error'),
|
|
||||||
{
|
|
||||||
variant: 'error',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -57,6 +49,16 @@ export default function ForgotPasswordForm() {
|
|||||||
margin="dense"
|
margin="dense"
|
||||||
autoComplete="username"
|
autoComplete="username"
|
||||||
/>
|
/>
|
||||||
|
{isError && (
|
||||||
|
<Alert severity="error" sx={{ mt: 2 }}>
|
||||||
|
{error?.message || formatMessage('forgotPasswordForm.error')}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
{isSuccess && (
|
||||||
|
<Alert severity="success" sx={{ mt: 2 }}>
|
||||||
|
{formatMessage('forgotPasswordForm.instructionsSent')}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
@@ -68,14 +70,6 @@ export default function ForgotPasswordForm() {
|
|||||||
>
|
>
|
||||||
{formatMessage('forgotPasswordForm.submit')}
|
{formatMessage('forgotPasswordForm.submit')}
|
||||||
</LoadingButton>
|
</LoadingButton>
|
||||||
{isSuccess && (
|
|
||||||
<Typography
|
|
||||||
variant="body1"
|
|
||||||
sx={{ color: (theme) => theme.palette.success.main }}
|
|
||||||
>
|
|
||||||
{formatMessage('forgotPasswordForm.instructionsSent')}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</Form>
|
</Form>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
|
@@ -13,12 +13,14 @@ function Form(props) {
|
|||||||
resolver,
|
resolver,
|
||||||
render,
|
render,
|
||||||
mode = 'all',
|
mode = 'all',
|
||||||
|
reValidateMode = 'onBlur',
|
||||||
|
automaticValidation = true,
|
||||||
...formProps
|
...formProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const methods = useForm({
|
const methods = useForm({
|
||||||
defaultValues,
|
defaultValues,
|
||||||
reValidateMode: 'onBlur',
|
reValidateMode,
|
||||||
resolver,
|
resolver,
|
||||||
mode,
|
mode,
|
||||||
});
|
});
|
||||||
@@ -30,7 +32,9 @@ function Form(props) {
|
|||||||
* For fields having `dependsOn` fields, we need to re-validate the form.
|
* For fields having `dependsOn` fields, we need to re-validate the form.
|
||||||
*/
|
*/
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
methods.trigger();
|
if (automaticValidation) {
|
||||||
|
methods.trigger();
|
||||||
|
}
|
||||||
}, [methods.trigger, form]);
|
}, [methods.trigger, form]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -56,6 +60,8 @@ Form.propTypes = {
|
|||||||
render: PropTypes.func,
|
render: PropTypes.func,
|
||||||
resolver: PropTypes.func,
|
resolver: PropTypes.func,
|
||||||
mode: PropTypes.oneOf(['onChange', 'onBlur', 'onSubmit', 'onTouched', 'all']),
|
mode: PropTypes.oneOf(['onChange', 'onBlur', 'onSubmit', 'onTouched', 'all']),
|
||||||
|
reValidateMode: PropTypes.oneOf(['onChange', 'onBlur', 'onSubmit']),
|
||||||
|
automaticValidation: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Form;
|
export default Form;
|
||||||
|
@@ -188,7 +188,7 @@ function InstallationForm() {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{install.isSuccess && (
|
{install.isSuccess && (
|
||||||
<Alert data-test="success-alert" severity="success" sx={{ mt: 3, fontWeight: 500 }}>
|
<Alert data-test="success-alert" severity="success" sx={{ mt: 3 }}>
|
||||||
{formatMessage('installationForm.success', {
|
{formatMessage('installationForm.success', {
|
||||||
link: (str) => (
|
link: (str) => (
|
||||||
<Link
|
<Link
|
||||||
|
@@ -2,6 +2,7 @@ import * as React from 'react';
|
|||||||
import { useNavigate, Link as RouterLink } from 'react-router-dom';
|
import { useNavigate, Link as RouterLink } from 'react-router-dom';
|
||||||
import Paper from '@mui/material/Paper';
|
import Paper from '@mui/material/Paper';
|
||||||
import Link from '@mui/material/Link';
|
import Link from '@mui/material/Link';
|
||||||
|
import Alert from '@mui/material/Alert';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import LoadingButton from '@mui/lab/LoadingButton';
|
import LoadingButton from '@mui/lab/LoadingButton';
|
||||||
import useAuthentication from 'hooks/useAuthentication';
|
import useAuthentication from 'hooks/useAuthentication';
|
||||||
@@ -11,16 +12,18 @@ import Form from 'components/Form';
|
|||||||
import TextField from 'components/TextField';
|
import TextField from 'components/TextField';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
import useCreateAccessToken from 'hooks/useCreateAccessToken';
|
import useCreateAccessToken from 'hooks/useCreateAccessToken';
|
||||||
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
|
||||||
|
|
||||||
function LoginForm() {
|
function LoginForm() {
|
||||||
const isCloud = useCloud();
|
const isCloud = useCloud();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const enqueueSnackbar = useEnqueueSnackbar();
|
|
||||||
const authentication = useAuthentication();
|
const authentication = useAuthentication();
|
||||||
const { mutateAsync: createAccessToken, isPending: loading } =
|
const {
|
||||||
useCreateAccessToken();
|
mutateAsync: createAccessToken,
|
||||||
|
isPending: loading,
|
||||||
|
error,
|
||||||
|
isError,
|
||||||
|
} = useCreateAccessToken();
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (authentication.isAuthenticated) {
|
if (authentication.isAuthenticated) {
|
||||||
@@ -37,11 +40,19 @@ function LoginForm() {
|
|||||||
});
|
});
|
||||||
const { token } = data;
|
const { token } = data;
|
||||||
authentication.updateToken(token);
|
authentication.updateToken(token);
|
||||||
} catch (error) {
|
} catch {}
|
||||||
enqueueSnackbar(error?.message || formatMessage('loginForm.error'), {
|
};
|
||||||
variant: 'error',
|
|
||||||
});
|
const renderError = () => {
|
||||||
}
|
const errors = error?.response?.data?.errors?.general || [
|
||||||
|
error?.message || formatMessage('loginForm.error'),
|
||||||
|
];
|
||||||
|
|
||||||
|
return errors.map((error) => (
|
||||||
|
<Alert severity="error" sx={{ mt: 2 }}>
|
||||||
|
{error}
|
||||||
|
</Alert>
|
||||||
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -94,6 +105,8 @@ function LoginForm() {
|
|||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{isError && renderError()}
|
||||||
|
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
|
@@ -0,0 +1,51 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useFormContext } from 'react-hook-form';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ControlledCheckbox from 'components/ControlledCheckbox';
|
||||||
|
|
||||||
|
const ActionField = ({ action, subject, disabled, name, syncIsCreator }) => {
|
||||||
|
const { formState, resetField } = useFormContext();
|
||||||
|
|
||||||
|
const actionDefaultValue =
|
||||||
|
formState.defaultValues?.[name]?.[subject.key]?.[action.key].value;
|
||||||
|
const conditionFieldName = `${name}.${subject.key}.${action.key}.conditions.isCreator`;
|
||||||
|
const conditionFieldTouched =
|
||||||
|
formState.touchedFields?.[name]?.[subject.key]?.[action.key]?.conditions
|
||||||
|
?.isCreator === true;
|
||||||
|
|
||||||
|
const handleSyncIsCreator = (newValue) => {
|
||||||
|
if (
|
||||||
|
syncIsCreator &&
|
||||||
|
actionDefaultValue === false &&
|
||||||
|
!conditionFieldTouched
|
||||||
|
) {
|
||||||
|
resetField(conditionFieldName, { defaultValue: newValue });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ControlledCheckbox
|
||||||
|
disabled={disabled}
|
||||||
|
name={`${name}.${subject.key}.${action.key}.value`}
|
||||||
|
dataTest={`${action.key.toLowerCase()}-checkbox`}
|
||||||
|
onChange={(e, value) => {
|
||||||
|
handleSyncIsCreator(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ActionField.propTypes = {
|
||||||
|
action: PropTypes.shape({
|
||||||
|
key: PropTypes.string.isRequired,
|
||||||
|
subjects: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||||
|
}),
|
||||||
|
subject: PropTypes.shape({
|
||||||
|
key: PropTypes.string.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
disabled: PropTypes.bool,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
syncIsCreator: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ActionField;
|
@@ -25,7 +25,6 @@ function PermissionSettings(props) {
|
|||||||
subject,
|
subject,
|
||||||
actions,
|
actions,
|
||||||
conditions,
|
conditions,
|
||||||
defaultChecked,
|
|
||||||
} = props;
|
} = props;
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const { getValues, resetField } = useFormContext();
|
const { getValues, resetField } = useFormContext();
|
||||||
@@ -34,7 +33,7 @@ function PermissionSettings(props) {
|
|||||||
for (const action of actions) {
|
for (const action of actions) {
|
||||||
for (const condition of conditions) {
|
for (const condition of conditions) {
|
||||||
const fieldName = `${fieldPrefix}.${action.key}.conditions.${condition.key}`;
|
const fieldName = `${fieldPrefix}.${action.key}.conditions.${condition.key}`;
|
||||||
resetField(fieldName);
|
resetField(fieldName, { keepTouched: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onClose();
|
onClose();
|
||||||
@@ -45,7 +44,7 @@ function PermissionSettings(props) {
|
|||||||
for (const condition of conditions) {
|
for (const condition of conditions) {
|
||||||
const fieldName = `${fieldPrefix}.${action.key}.conditions.${condition.key}`;
|
const fieldName = `${fieldPrefix}.${action.key}.conditions.${condition.key}`;
|
||||||
const value = getValues(fieldName);
|
const value = getValues(fieldName);
|
||||||
resetField(fieldName, { defaultValue: value });
|
resetField(fieldName, { defaultValue: value, keepTouched: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onClose();
|
onClose();
|
||||||
@@ -56,6 +55,7 @@ function PermissionSettings(props) {
|
|||||||
open={open}
|
open={open}
|
||||||
onClose={cancel}
|
onClose={cancel}
|
||||||
data-test={`${subject}-role-conditions-modal`}
|
data-test={`${subject}-role-conditions-modal`}
|
||||||
|
keepMounted
|
||||||
>
|
>
|
||||||
<DialogTitle>{formatMessage('permissionSettings.title')}</DialogTitle>
|
<DialogTitle>{formatMessage('permissionSettings.title')}</DialogTitle>
|
||||||
|
|
||||||
@@ -65,10 +65,10 @@ function PermissionSettings(props) {
|
|||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell component="th" />
|
<TableCell component="th" />
|
||||||
|
|
||||||
{actions.map((action) => (
|
{actions.map((action) => (
|
||||||
<TableCell component="th" key={action.key}>
|
<TableCell component="th" key={action.key}>
|
||||||
<Typography
|
<Typography
|
||||||
|
component="div"
|
||||||
variant="subtitle1"
|
variant="subtitle1"
|
||||||
align="center"
|
align="center"
|
||||||
sx={{
|
sx={{
|
||||||
@@ -89,7 +89,7 @@ function PermissionSettings(props) {
|
|||||||
sx={{ '&:last-child td': { border: 0 } }}
|
sx={{ '&:last-child td': { border: 0 } }}
|
||||||
>
|
>
|
||||||
<TableCell scope="row">
|
<TableCell scope="row">
|
||||||
<Typography variant="subtitle2">
|
<Typography variant="subtitle2" component="div">
|
||||||
{condition.label}
|
{condition.label}
|
||||||
</Typography>
|
</Typography>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -99,14 +99,13 @@ function PermissionSettings(props) {
|
|||||||
key={`${action.key}.${condition.key}`}
|
key={`${action.key}.${condition.key}`}
|
||||||
align="center"
|
align="center"
|
||||||
>
|
>
|
||||||
<Typography variant="subtitle2">
|
<Typography variant="subtitle2" component="div">
|
||||||
{action.subjects.includes(subject) && (
|
{action.subjects.includes(subject) && (
|
||||||
<ControlledCheckbox
|
<ControlledCheckbox
|
||||||
name={`${fieldPrefix}.${action.key}.conditions.${condition.key}`}
|
name={`${fieldPrefix}.${action.key}.conditions.${condition.key}`}
|
||||||
dataTest={`${
|
dataTest={`${
|
||||||
condition.key
|
condition.key
|
||||||
}-${action.key.toLowerCase()}-checkbox`}
|
}-${action.key.toLowerCase()}-checkbox`}
|
||||||
defaultValue={defaultChecked}
|
|
||||||
disabled={
|
disabled={
|
||||||
getValues(
|
getValues(
|
||||||
`${fieldPrefix}.${action.key}.value`,
|
`${fieldPrefix}.${action.key}.value`,
|
||||||
@@ -144,7 +143,6 @@ PermissionSettings.propTypes = {
|
|||||||
fieldPrefix: PropTypes.string.isRequired,
|
fieldPrefix: PropTypes.string.isRequired,
|
||||||
subject: PropTypes.string.isRequired,
|
subject: PropTypes.string.isRequired,
|
||||||
open: PropTypes.bool,
|
open: PropTypes.bool,
|
||||||
defaultChecked: PropTypes.bool,
|
|
||||||
actions: PropTypes.arrayOf(
|
actions: PropTypes.arrayOf(
|
||||||
PropTypes.shape({
|
PropTypes.shape({
|
||||||
label: PropTypes.string,
|
label: PropTypes.string,
|
||||||
|
@@ -12,15 +12,15 @@ import TableRow from '@mui/material/TableRow';
|
|||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
import ControlledCheckbox from 'components/ControlledCheckbox';
|
|
||||||
import usePermissionCatalog from 'hooks/usePermissionCatalog.ee';
|
import usePermissionCatalog from 'hooks/usePermissionCatalog.ee';
|
||||||
import PermissionSettings from './PermissionSettings.ee';
|
import PermissionSettings from './PermissionSettings.ee';
|
||||||
import PermissionCatalogFieldLoader from './PermissionCatalogFieldLoader';
|
import PermissionCatalogFieldLoader from './PermissionCatalogFieldLoader';
|
||||||
|
import ActionField from './ActionField';
|
||||||
|
|
||||||
const PermissionCatalogField = ({
|
const PermissionCatalogField = ({
|
||||||
name = 'permissions',
|
name = 'permissions',
|
||||||
disabled = false,
|
disabled = false,
|
||||||
defaultChecked = false,
|
syncIsCreator = false,
|
||||||
}) => {
|
}) => {
|
||||||
const { data, isLoading: isPermissionCatalogLoading } =
|
const { data, isLoading: isPermissionCatalogLoading } =
|
||||||
usePermissionCatalog();
|
usePermissionCatalog();
|
||||||
@@ -30,7 +30,7 @@ const PermissionCatalogField = ({
|
|||||||
if (isPermissionCatalogLoading) return <PermissionCatalogFieldLoader />;
|
if (isPermissionCatalogLoading) return <PermissionCatalogFieldLoader />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableContainer component={Paper}>
|
<TableContainer data-test="permissions-catalog" component={Paper}>
|
||||||
<Table>
|
<Table>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
@@ -39,6 +39,7 @@ const PermissionCatalogField = ({
|
|||||||
{permissionCatalog?.actions.map((action) => (
|
{permissionCatalog?.actions.map((action) => (
|
||||||
<TableCell component="th" key={action.key}>
|
<TableCell component="th" key={action.key}>
|
||||||
<Typography
|
<Typography
|
||||||
|
component="div"
|
||||||
variant="subtitle1"
|
variant="subtitle1"
|
||||||
align="center"
|
align="center"
|
||||||
sx={{
|
sx={{
|
||||||
@@ -62,20 +63,23 @@ const PermissionCatalogField = ({
|
|||||||
data-test={`${subject.key}-permission-row`}
|
data-test={`${subject.key}-permission-row`}
|
||||||
>
|
>
|
||||||
<TableCell scope="row">
|
<TableCell scope="row">
|
||||||
<Typography variant="subtitle2">{subject.label}</Typography>
|
<Typography variant="subtitle2" component="div">
|
||||||
|
{subject.label}
|
||||||
|
</Typography>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
{permissionCatalog?.actions.map((action) => (
|
{permissionCatalog?.actions.map((action) => (
|
||||||
<TableCell key={`${subject.key}.${action.key}`} align="center">
|
<TableCell key={`${subject.key}.${action.key}`} align="center">
|
||||||
<Typography variant="subtitle2">
|
<Typography variant="subtitle2" component="div">
|
||||||
{action.subjects.includes(subject.key) && (
|
{action.subjects.includes(subject.key) && (
|
||||||
<ControlledCheckbox
|
<ActionField
|
||||||
|
action={action}
|
||||||
|
subject={subject}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
name={`${name}.${subject.key}.${action.key}.value`}
|
name={name}
|
||||||
dataTest={`${action.key.toLowerCase()}-checkbox`}
|
syncIsCreator={syncIsCreator}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!action.subjects.includes(subject.key) && '-'}
|
{!action.subjects.includes(subject.key) && '-'}
|
||||||
</Typography>
|
</Typography>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -100,7 +104,6 @@ const PermissionCatalogField = ({
|
|||||||
subject={subject.key}
|
subject={subject.key}
|
||||||
actions={permissionCatalog?.actions}
|
actions={permissionCatalog?.actions}
|
||||||
conditions={permissionCatalog?.conditions}
|
conditions={permissionCatalog?.conditions}
|
||||||
defaultChecked={defaultChecked}
|
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -114,7 +117,7 @@ const PermissionCatalogField = ({
|
|||||||
PermissionCatalogField.propTypes = {
|
PermissionCatalogField.propTypes = {
|
||||||
name: PropTypes.string,
|
name: PropTypes.string,
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
defaultChecked: PropTypes.bool,
|
syncIsCreator: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PermissionCatalogField;
|
export default PermissionCatalogField;
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ClickAwayListener from '@mui/base/ClickAwayListener';
|
import { ClickAwayListener } from '@mui/base/ClickAwayListener';
|
||||||
import FormHelperText from '@mui/material/FormHelperText';
|
import FormHelperText from '@mui/material/FormHelperText';
|
||||||
import InputLabel from '@mui/material/InputLabel';
|
import InputLabel from '@mui/material/InputLabel';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
@@ -2,6 +2,7 @@ import { yupResolver } from '@hookform/resolvers/yup';
|
|||||||
import LoadingButton from '@mui/lab/LoadingButton';
|
import LoadingButton from '@mui/lab/LoadingButton';
|
||||||
import Paper from '@mui/material/Paper';
|
import Paper from '@mui/material/Paper';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
|
import Alert from '@mui/material/Alert';
|
||||||
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
@@ -30,6 +31,8 @@ export default function ResetPasswordForm() {
|
|||||||
mutateAsync: resetPassword,
|
mutateAsync: resetPassword,
|
||||||
isPending,
|
isPending,
|
||||||
isSuccess,
|
isSuccess,
|
||||||
|
error,
|
||||||
|
isError,
|
||||||
} = useResetPassword();
|
} = useResetPassword();
|
||||||
const token = searchParams.get('token');
|
const token = searchParams.get('token');
|
||||||
|
|
||||||
@@ -47,14 +50,23 @@ export default function ResetPasswordForm() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
navigate(URLS.LOGIN);
|
navigate(URLS.LOGIN);
|
||||||
} catch (error) {
|
} catch {}
|
||||||
enqueueSnackbar(
|
};
|
||||||
error?.message || formatMessage('resetPasswordForm.error'),
|
|
||||||
{
|
const renderError = () => {
|
||||||
variant: 'error',
|
if (!isError) {
|
||||||
},
|
return null;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const errors = error?.response?.data?.errors?.general || [
|
||||||
|
error?.message || formatMessage('resetPasswordForm.error'),
|
||||||
|
];
|
||||||
|
|
||||||
|
return errors.map((error) => (
|
||||||
|
<Alert severity="error" sx={{ mt: 2 }}>
|
||||||
|
{error}
|
||||||
|
</Alert>
|
||||||
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -96,7 +108,6 @@ export default function ResetPasswordForm() {
|
|||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
label={formatMessage(
|
label={formatMessage(
|
||||||
'resetPasswordForm.confirmPasswordFieldLabel',
|
'resetPasswordForm.confirmPasswordFieldLabel',
|
||||||
@@ -117,7 +128,7 @@ export default function ResetPasswordForm() {
|
|||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
{renderError()}
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
|
@@ -7,7 +7,7 @@ import FormControl from '@mui/material/FormControl';
|
|||||||
import SearchIcon from '@mui/icons-material/Search';
|
import SearchIcon from '@mui/icons-material/Search';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
|
|
||||||
export default function SearchInput({ onChange }) {
|
export default function SearchInput({ onChange, defaultValue = '' }) {
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
return (
|
return (
|
||||||
<FormControl variant="outlined" fullWidth>
|
<FormControl variant="outlined" fullWidth>
|
||||||
@@ -16,6 +16,7 @@ export default function SearchInput({ onChange }) {
|
|||||||
</InputLabel>
|
</InputLabel>
|
||||||
|
|
||||||
<OutlinedInput
|
<OutlinedInput
|
||||||
|
defaultValue={defaultValue}
|
||||||
id="search-input"
|
id="search-input"
|
||||||
type="text"
|
type="text"
|
||||||
size="medium"
|
size="medium"
|
||||||
@@ -34,4 +35,5 @@ export default function SearchInput({ onChange }) {
|
|||||||
|
|
||||||
SearchInput.propTypes = {
|
SearchInput.propTypes = {
|
||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
|
defaultValue: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
@@ -7,9 +7,9 @@ function Variable({ attributes, children, element, disabled }) {
|
|||||||
const focused = useFocused();
|
const focused = useFocused();
|
||||||
const label = (
|
const label = (
|
||||||
<>
|
<>
|
||||||
|
{children}
|
||||||
<span style={{ fontWeight: 500 }}>{element.name}</span>:{' '}
|
<span style={{ fontWeight: 500 }}>{element.name}</span>:{' '}
|
||||||
<span style={{ fontWeight: 300 }}>{element.sampleValue}</span>
|
<span style={{ fontWeight: 300 }}>{element.sampleValue}</span>
|
||||||
{children}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
|
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
|||||||
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import ButtonGroup from '@mui/material/ButtonGroup';
|
import ButtonGroup from '@mui/material/ButtonGroup';
|
||||||
import ClickAwayListener from '@mui/material/ClickAwayListener';
|
import { ClickAwayListener } from '@mui/base/ClickAwayListener';
|
||||||
import Grow from '@mui/material/Grow';
|
import Grow from '@mui/material/Grow';
|
||||||
import MenuItem from '@mui/material/MenuItem';
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
import MenuList from '@mui/material/MenuList';
|
import MenuList from '@mui/material/MenuList';
|
||||||
|
@@ -11,14 +11,17 @@ export default function SubscriptionCancelledAlert() {
|
|||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const subscription = useSubscription();
|
const subscription = useSubscription();
|
||||||
const trial = useUserTrial();
|
const trial = useUserTrial();
|
||||||
|
|
||||||
if (subscription?.data?.status === 'active' || trial.hasTrial)
|
|
||||||
return <React.Fragment />;
|
|
||||||
|
|
||||||
const cancellationEffectiveDateObject = DateTime.fromISO(
|
const cancellationEffectiveDateObject = DateTime.fromISO(
|
||||||
subscription?.data?.cancellationEffectiveDate,
|
subscription?.data?.cancellationEffectiveDate,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
subscription?.data?.status === 'active' ||
|
||||||
|
trial.hasTrial ||
|
||||||
|
!cancellationEffectiveDateObject.isValid
|
||||||
|
)
|
||||||
|
return <React.Fragment />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Alert
|
<Alert
|
||||||
severity="warning"
|
severity="warning"
|
||||||
|
@@ -84,10 +84,7 @@ function TestSubstep(props) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{hasError && (
|
{hasError && (
|
||||||
<Alert
|
<Alert severity="error" sx={{ mb: 2, width: '100%' }}>
|
||||||
severity="error"
|
|
||||||
sx={{ mb: 2, fontWeight: 500, width: '100%' }}
|
|
||||||
>
|
|
||||||
<pre style={{ margin: 0, whiteSpace: 'pre-wrap' }}>
|
<pre style={{ margin: 0, whiteSpace: 'pre-wrap' }}>
|
||||||
{JSON.stringify(errorDetails, null, 2)}
|
{JSON.stringify(errorDetails, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
@@ -104,13 +101,11 @@ function TestSubstep(props) {
|
|||||||
severity="warning"
|
severity="warning"
|
||||||
sx={{ mb: 1, width: '100%' }}
|
sx={{ mb: 1, width: '100%' }}
|
||||||
>
|
>
|
||||||
<AlertTitle sx={{ fontWeight: 700 }}>
|
<AlertTitle>
|
||||||
{formatMessage('flowEditor.noTestDataTitle')}
|
{formatMessage('flowEditor.noTestDataTitle')}
|
||||||
</AlertTitle>
|
</AlertTitle>
|
||||||
|
|
||||||
<Box sx={{ fontWeight: 400 }}>
|
<Box>{formatMessage('flowEditor.noTestDataMessage')}</Box>
|
||||||
{formatMessage('flowEditor.noTestDataMessage')}
|
|
||||||
</Box>
|
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@@ -31,6 +31,7 @@ function TextField(props) {
|
|||||||
onBlur,
|
onBlur,
|
||||||
onChange,
|
onChange,
|
||||||
'data-test': dataTest,
|
'data-test': dataTest,
|
||||||
|
showError = false,
|
||||||
...textFieldProps
|
...textFieldProps
|
||||||
} = props;
|
} = props;
|
||||||
return (
|
return (
|
||||||
@@ -47,6 +48,7 @@ function TextField(props) {
|
|||||||
onBlur: controllerOnBlur,
|
onBlur: controllerOnBlur,
|
||||||
...field
|
...field
|
||||||
},
|
},
|
||||||
|
fieldState: { error },
|
||||||
}) => (
|
}) => (
|
||||||
<MuiTextField
|
<MuiTextField
|
||||||
{...textFieldProps}
|
{...textFieldProps}
|
||||||
@@ -72,6 +74,7 @@ function TextField(props) {
|
|||||||
inputProps={{
|
inputProps={{
|
||||||
'data-test': dataTest,
|
'data-test': dataTest,
|
||||||
}}
|
}}
|
||||||
|
{...(showError && { helperText: error?.message, error: !!error })}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -89,6 +92,7 @@ TextField.propTypes = {
|
|||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
onBlur: PropTypes.func,
|
onBlur: PropTypes.func,
|
||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
|
showError: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TextField;
|
export default TextField;
|
||||||
|
@@ -44,7 +44,7 @@ function BillingCard(props) {
|
|||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography variant="h6" fontWeight="bold">
|
<Typography variant="h6" fontWeight="bold">
|
||||||
{title}
|
{title || '---'}
|
||||||
</Typography>
|
</Typography>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
@@ -119,12 +119,12 @@ export default function UsageDataInformation() {
|
|||||||
text: 'Upgrade plan',
|
text: 'Upgrade plan',
|
||||||
},
|
},
|
||||||
nextBillAmount: {
|
nextBillAmount: {
|
||||||
title: '---',
|
title: null,
|
||||||
action: null,
|
action: null,
|
||||||
text: null,
|
text: null,
|
||||||
},
|
},
|
||||||
nextBillDate: {
|
nextBillDate: {
|
||||||
title: '---',
|
title: null,
|
||||||
action: null,
|
action: null,
|
||||||
text: null,
|
text: null,
|
||||||
},
|
},
|
||||||
@@ -137,7 +137,9 @@ export default function UsageDataInformation() {
|
|||||||
text: formatMessage('usageDataInformation.cancelPlan'),
|
text: formatMessage('usageDataInformation.cancelPlan'),
|
||||||
},
|
},
|
||||||
nextBillAmount: {
|
nextBillAmount: {
|
||||||
title: `€${subscription?.nextBillAmount}`,
|
title: subscription?.nextBillAmount
|
||||||
|
? `€${subscription?.nextBillAmount}`
|
||||||
|
: null,
|
||||||
action: subscription?.updateUrl,
|
action: subscription?.updateUrl,
|
||||||
text: formatMessage('usageDataInformation.updatePaymentMethod'),
|
text: formatMessage('usageDataInformation.updatePaymentMethod'),
|
||||||
},
|
},
|
||||||
|
@@ -45,3 +45,36 @@ export function getPermissions(computedPermissions) {
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getComputedPermissionsDefaultValues = (
|
||||||
|
data,
|
||||||
|
conditionsInitialValues,
|
||||||
|
) => {
|
||||||
|
if (!data) return {};
|
||||||
|
|
||||||
|
const conditions = {};
|
||||||
|
data.conditions.forEach((condition) => {
|
||||||
|
conditions[condition.key] =
|
||||||
|
conditionsInitialValues?.[condition.key] || false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = {};
|
||||||
|
|
||||||
|
data.subjects.forEach((subject) => {
|
||||||
|
const subjectKey = subject.key;
|
||||||
|
result[subjectKey] = {};
|
||||||
|
|
||||||
|
data.actions.forEach((action) => {
|
||||||
|
const actionKey = action.key;
|
||||||
|
|
||||||
|
if (action.subjects.includes(subjectKey)) {
|
||||||
|
result[subjectKey][actionKey] = {
|
||||||
|
value: false,
|
||||||
|
conditions: { ...conditions },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
@@ -5,6 +5,8 @@ import Stack from '@mui/material/Stack';
|
|||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
|
import * as yup from 'yup';
|
||||||
|
|
||||||
import Form from 'components/Form';
|
import Form from 'components/Form';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
@@ -23,6 +25,42 @@ function generateFormRoleMappings(roleMappings) {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const uniqueRemoteRoleName = (array, context, formatMessage) => {
|
||||||
|
const seen = new Set();
|
||||||
|
for (const [index, value] of array.entries()) {
|
||||||
|
if (seen.has(value.remoteRoleName)) {
|
||||||
|
const path = `${context.path}[${index}].remoteRoleName`;
|
||||||
|
return context.createError({
|
||||||
|
message: `${formatMessage('roleMappingsForm.remoteRoleName')} must be unique`,
|
||||||
|
path,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
seen.add(value.remoteRoleName);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getValidationSchema = (formatMessage) =>
|
||||||
|
yup.object({
|
||||||
|
roleMappings: yup
|
||||||
|
.array()
|
||||||
|
.of(
|
||||||
|
yup.object({
|
||||||
|
roleId: yup
|
||||||
|
.string()
|
||||||
|
.required(`${formatMessage('roleMappingsForm.role')} is required`),
|
||||||
|
remoteRoleName: yup
|
||||||
|
.string()
|
||||||
|
.required(
|
||||||
|
`${formatMessage('roleMappingsForm.remoteRoleName')} is required`,
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.test('unique-remoteRoleName', '', (value, ctx) => {
|
||||||
|
return uniqueRemoteRoleName(value, ctx, formatMessage);
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
function RoleMappings({ provider, providerLoading }) {
|
function RoleMappings({ provider, providerLoading }) {
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const enqueueSnackbar = useEnqueueSnackbar();
|
const enqueueSnackbar = useEnqueueSnackbar();
|
||||||
@@ -94,7 +132,15 @@ function RoleMappings({ provider, providerLoading }) {
|
|||||||
<Typography variant="h3">
|
<Typography variant="h3">
|
||||||
{formatMessage('roleMappingsForm.title')}
|
{formatMessage('roleMappingsForm.title')}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Form defaultValues={defaultValues} onSubmit={handleRoleMappingsUpdate}>
|
<Form
|
||||||
|
defaultValues={defaultValues}
|
||||||
|
onSubmit={handleRoleMappingsUpdate}
|
||||||
|
resolver={yupResolver(getValidationSchema(formatMessage))}
|
||||||
|
mode="onSubmit"
|
||||||
|
reValidateMode="onChange"
|
||||||
|
noValidate
|
||||||
|
automaticValidation={false}
|
||||||
|
>
|
||||||
<Stack direction="column" spacing={2}>
|
<Stack direction="column" spacing={2}>
|
||||||
<RoleMappingsFieldArray />
|
<RoleMappingsFieldArray />
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
|
@@ -55,6 +55,7 @@ function RoleMappingsFieldArray() {
|
|||||||
label={formatMessage('roleMappingsForm.remoteRoleName')}
|
label={formatMessage('roleMappingsForm.remoteRoleName')}
|
||||||
fullWidth
|
fullWidth
|
||||||
required
|
required
|
||||||
|
showError
|
||||||
/>
|
/>
|
||||||
<ControlledAutocomplete
|
<ControlledAutocomplete
|
||||||
name={`roleMappings.${index}.roleId`}
|
name={`roleMappings.${index}.roleId`}
|
||||||
@@ -62,14 +63,17 @@ function RoleMappingsFieldArray() {
|
|||||||
disablePortal
|
disablePortal
|
||||||
disableClearable
|
disableClearable
|
||||||
options={generateRoleOptions(roles)}
|
options={generateRoleOptions(roles)}
|
||||||
renderInput={(params) => (
|
renderInput={(params, { error }) => (
|
||||||
<MuiTextField
|
<MuiTextField
|
||||||
{...params}
|
{...params}
|
||||||
label={formatMessage('roleMappingsForm.role')}
|
label={formatMessage('roleMappingsForm.role')}
|
||||||
required
|
required
|
||||||
|
error={!!error}
|
||||||
|
helperText={error?.message}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
loading={isRolesLoading}
|
loading={isRolesLoading}
|
||||||
|
showHelperText={false}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
@@ -11,9 +11,13 @@ import Form from 'components/Form';
|
|||||||
import PageTitle from 'components/PageTitle';
|
import PageTitle from 'components/PageTitle';
|
||||||
import TextField from 'components/TextField';
|
import TextField from 'components/TextField';
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
import { getPermissions } from 'helpers/computePermissions.ee';
|
import {
|
||||||
|
getComputedPermissionsDefaultValues,
|
||||||
|
getPermissions,
|
||||||
|
} from 'helpers/computePermissions.ee';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
import useAdminCreateRole from 'hooks/useAdminCreateRole';
|
import useAdminCreateRole from 'hooks/useAdminCreateRole';
|
||||||
|
import usePermissionCatalog from 'hooks/usePermissionCatalog.ee';
|
||||||
|
|
||||||
export default function CreateRole() {
|
export default function CreateRole() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -21,6 +25,21 @@ export default function CreateRole() {
|
|||||||
const enqueueSnackbar = useEnqueueSnackbar();
|
const enqueueSnackbar = useEnqueueSnackbar();
|
||||||
const { mutateAsync: createRole, isPending: isCreateRolePending } =
|
const { mutateAsync: createRole, isPending: isCreateRolePending } =
|
||||||
useAdminCreateRole();
|
useAdminCreateRole();
|
||||||
|
const { data: permissionCatalogData } = usePermissionCatalog();
|
||||||
|
|
||||||
|
const defaultValues = React.useMemo(
|
||||||
|
() => ({
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
computedPermissions: getComputedPermissionsDefaultValues(
|
||||||
|
permissionCatalogData?.data,
|
||||||
|
{
|
||||||
|
isCreator: true,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
[permissionCatalogData],
|
||||||
|
);
|
||||||
|
|
||||||
const handleRoleCreation = async (roleData) => {
|
const handleRoleCreation = async (roleData) => {
|
||||||
try {
|
try {
|
||||||
@@ -64,7 +83,7 @@ export default function CreateRole() {
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid item xs={12} justifyContent="flex-end" sx={{ pt: 5 }}>
|
<Grid item xs={12} justifyContent="flex-end" sx={{ pt: 5 }}>
|
||||||
<Form onSubmit={handleRoleCreation}>
|
<Form onSubmit={handleRoleCreation} defaultValues={defaultValues}>
|
||||||
<Stack direction="column" gap={2}>
|
<Stack direction="column" gap={2}>
|
||||||
<TextField
|
<TextField
|
||||||
required={true}
|
required={true}
|
||||||
@@ -81,10 +100,7 @@ export default function CreateRole() {
|
|||||||
data-test="description-input"
|
data-test="description-input"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PermissionCatalogField
|
<PermissionCatalogField name="computedPermissions" />
|
||||||
name="computedPermissions"
|
|
||||||
defaultChecked={true}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
type="submit"
|
type="submit"
|
||||||
|
@@ -124,7 +124,6 @@ export default function CreateUser() {
|
|||||||
<Alert
|
<Alert
|
||||||
severity="info"
|
severity="info"
|
||||||
color="primary"
|
color="primary"
|
||||||
sx={{ fontWeight: '500' }}
|
|
||||||
data-test="invitation-email-info-alert"
|
data-test="invitation-email-info-alert"
|
||||||
>
|
>
|
||||||
{formatMessage('createUser.invitationEmailInfo', {
|
{formatMessage('createUser.invitationEmailInfo', {
|
||||||
|
@@ -5,6 +5,7 @@ import Stack from '@mui/material/Stack';
|
|||||||
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
import { merge } from 'lodash';
|
||||||
|
|
||||||
import Container from 'components/Container';
|
import Container from 'components/Container';
|
||||||
import Form from 'components/Form';
|
import Form from 'components/Form';
|
||||||
@@ -13,21 +14,25 @@ import PermissionCatalogField from 'components/PermissionCatalogField/index.ee';
|
|||||||
import TextField from 'components/TextField';
|
import TextField from 'components/TextField';
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
import {
|
import {
|
||||||
|
getComputedPermissionsDefaultValues,
|
||||||
getPermissions,
|
getPermissions,
|
||||||
getRoleWithComputedPermissions,
|
getRoleWithComputedPermissions,
|
||||||
} from 'helpers/computePermissions.ee';
|
} from 'helpers/computePermissions.ee';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
import useAdminUpdateRole from 'hooks/useAdminUpdateRole';
|
import useAdminUpdateRole from 'hooks/useAdminUpdateRole';
|
||||||
import useRole from 'hooks/useRole.ee';
|
import useRole from 'hooks/useRole.ee';
|
||||||
|
import usePermissionCatalog from 'hooks/usePermissionCatalog.ee';
|
||||||
|
|
||||||
export default function EditRole() {
|
export default function EditRole() {
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { roleId } = useParams();
|
const { roleId } = useParams();
|
||||||
const { data, loading: isRoleLoading } = useRole({ roleId });
|
const { data: roleData, isLoading: isRoleLoading } = useRole({ roleId });
|
||||||
const { mutateAsync: updateRole, isPending: isUpdateRolePending } =
|
const { mutateAsync: updateRole, isPending: isUpdateRolePending } =
|
||||||
useAdminUpdateRole(roleId);
|
useAdminUpdateRole(roleId);
|
||||||
const role = data?.data;
|
const { data: permissionCatalogData } = usePermissionCatalog();
|
||||||
|
const role = roleData?.data;
|
||||||
|
const permissionCatalog = permissionCatalogData?.data;
|
||||||
const enqueueSnackbar = useEnqueueSnackbar();
|
const enqueueSnackbar = useEnqueueSnackbar();
|
||||||
|
|
||||||
const handleRoleUpdate = async (roleData) => {
|
const handleRoleUpdate = async (roleData) => {
|
||||||
@@ -52,7 +57,20 @@ export default function EditRole() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const roleWithComputedPermissions = getRoleWithComputedPermissions(role);
|
const defaultValues = React.useMemo(() => {
|
||||||
|
const roleWithComputedPermissions = getRoleWithComputedPermissions(role);
|
||||||
|
const computedPermissionsDefaultValues =
|
||||||
|
getComputedPermissionsDefaultValues(permissionCatalog);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...roleWithComputedPermissions,
|
||||||
|
computedPermissions: merge(
|
||||||
|
{},
|
||||||
|
computedPermissionsDefaultValues,
|
||||||
|
roleWithComputedPermissions.computedPermissions,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}, [role, permissionCatalog]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container sx={{ py: 3, display: 'flex', justifyContent: 'center' }}>
|
<Container sx={{ py: 3, display: 'flex', justifyContent: 'center' }}>
|
||||||
@@ -64,10 +82,7 @@ export default function EditRole() {
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid item xs={12} justifyContent="flex-end" sx={{ pt: 5 }}>
|
<Grid item xs={12} justifyContent="flex-end" sx={{ pt: 5 }}>
|
||||||
<Form
|
<Form defaultValues={defaultValues} onSubmit={handleRoleUpdate}>
|
||||||
defaultValues={roleWithComputedPermissions}
|
|
||||||
onSubmit={handleRoleUpdate}
|
|
||||||
>
|
|
||||||
<Stack direction="column" gap={2}>
|
<Stack direction="column" gap={2}>
|
||||||
{isRoleLoading && (
|
{isRoleLoading && (
|
||||||
<>
|
<>
|
||||||
@@ -95,12 +110,11 @@ export default function EditRole() {
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<PermissionCatalogField
|
<PermissionCatalogField
|
||||||
name="computedPermissions"
|
name="computedPermissions"
|
||||||
disabled={role?.isAdmin}
|
disabled={role?.isAdmin}
|
||||||
|
syncIsCreator
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
|
@@ -42,13 +42,9 @@ export default function Execution() {
|
|||||||
<Grid container item sx={{ mt: 2, mb: [2, 5] }} rowGap={3}>
|
<Grid container item sx={{ mt: 2, mb: [2, 5] }} rowGap={3}>
|
||||||
{!isExecutionStepsLoading && !data?.pages?.[0].data.length && (
|
{!isExecutionStepsLoading && !data?.pages?.[0].data.length && (
|
||||||
<Alert severity="warning" sx={{ flex: 1 }}>
|
<Alert severity="warning" sx={{ flex: 1 }}>
|
||||||
<AlertTitle sx={{ fontWeight: 700 }}>
|
<AlertTitle>{formatMessage('execution.noDataTitle')}</AlertTitle>
|
||||||
{formatMessage('execution.noDataTitle')}
|
|
||||||
</AlertTitle>
|
|
||||||
|
|
||||||
<Box sx={{ fontWeight: 400 }}>
|
<Box>{formatMessage('execution.noDataMessage')}</Box>
|
||||||
{formatMessage('execution.noDataMessage')}
|
|
||||||
</Box>
|
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Link, useSearchParams } from 'react-router-dom';
|
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Grid from '@mui/material/Grid';
|
import Grid from '@mui/material/Grid';
|
||||||
@@ -23,13 +23,18 @@ import useLazyFlows from 'hooks/useLazyFlows';
|
|||||||
|
|
||||||
export default function Flows() {
|
export default function Flows() {
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
|
const navigate = useNavigate();
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const page = parseInt(searchParams.get('page') || '', 10) || 1;
|
const page = parseInt(searchParams.get('page') || '', 10) || 1;
|
||||||
const [flowName, setFlowName] = React.useState('');
|
const flowName = searchParams.get('flowName') || '';
|
||||||
const [isLoading, setIsLoading] = React.useState(false);
|
const [isLoading, setIsLoading] = React.useState(true);
|
||||||
const currentUserAbility = useCurrentUserAbility();
|
const currentUserAbility = useCurrentUserAbility();
|
||||||
|
|
||||||
const { data, mutate: fetchFlows } = useLazyFlows(
|
const {
|
||||||
|
data,
|
||||||
|
mutate: fetchFlows,
|
||||||
|
isSuccess,
|
||||||
|
} = useLazyFlows(
|
||||||
{ flowName, page },
|
{ flowName, page },
|
||||||
{
|
{
|
||||||
onSettled: () => {
|
onSettled: () => {
|
||||||
@@ -38,6 +43,36 @@ export default function Flows() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const flows = data?.data || [];
|
||||||
|
const pageInfo = data?.meta;
|
||||||
|
const hasFlows = flows?.length;
|
||||||
|
const navigateToLastPage = isSuccess && !hasFlows && page > 1;
|
||||||
|
|
||||||
|
const onSearchChange = React.useCallback((event) => {
|
||||||
|
setSearchParams({ flowName: event.target.value });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getPathWithSearchParams = (page, flowName) => {
|
||||||
|
const searchParams = new URLSearchParams();
|
||||||
|
|
||||||
|
if (page > 1) {
|
||||||
|
searchParams.set('page', page);
|
||||||
|
}
|
||||||
|
if (flowName) {
|
||||||
|
searchParams.set('flowName', flowName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { search: searchParams.toString() };
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDuplicateFlow = () => {
|
||||||
|
if (pageInfo?.currentPage > 1) {
|
||||||
|
navigate(getPathWithSearchParams(1, flowName));
|
||||||
|
} else {
|
||||||
|
fetchFlows();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const fetchData = React.useMemo(
|
const fetchData = React.useMemo(
|
||||||
() => debounce(fetchFlows, 300),
|
() => debounce(fetchFlows, 300),
|
||||||
[fetchFlows],
|
[fetchFlows],
|
||||||
@@ -54,21 +89,14 @@ export default function Flows() {
|
|||||||
}, [fetchData, flowName, page]);
|
}, [fetchData, flowName, page]);
|
||||||
|
|
||||||
React.useEffect(
|
React.useEffect(
|
||||||
function resetPageOnSearch() {
|
function redirectToLastPage() {
|
||||||
// reset search params which only consists of `page`
|
if (navigateToLastPage) {
|
||||||
setSearchParams({});
|
navigate(getPathWithSearchParams(pageInfo.totalPages, flowName));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[flowName],
|
[navigateToLastPage],
|
||||||
);
|
);
|
||||||
|
|
||||||
const flows = data?.data || [];
|
|
||||||
const pageInfo = data?.meta;
|
|
||||||
const hasFlows = flows?.length;
|
|
||||||
|
|
||||||
const onSearchChange = React.useCallback((event) => {
|
|
||||||
setFlowName(event.target.value);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ py: 3 }}>
|
<Box sx={{ py: 3 }}>
|
||||||
<Container>
|
<Container>
|
||||||
@@ -78,7 +106,7 @@ export default function Flows() {
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid item xs={12} sm="auto" order={{ xs: 2, sm: 1 }}>
|
<Grid item xs={12} sm="auto" order={{ xs: 2, sm: 1 }}>
|
||||||
<SearchInput onChange={onSearchChange} />
|
<SearchInput onChange={onSearchChange} defaultValue={flowName} />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid
|
<Grid
|
||||||
@@ -111,7 +139,7 @@ export default function Flows() {
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Divider sx={{ mt: [2, 0], mb: 2 }} />
|
<Divider sx={{ mt: [2, 0], mb: 2 }} />
|
||||||
{isLoading && (
|
{(isLoading || navigateToLastPage) && (
|
||||||
<CircularProgress sx={{ display: 'block', margin: '20px auto' }} />
|
<CircularProgress sx={{ display: 'block', margin: '20px auto' }} />
|
||||||
)}
|
)}
|
||||||
{!isLoading &&
|
{!isLoading &&
|
||||||
@@ -119,11 +147,11 @@ export default function Flows() {
|
|||||||
<FlowRow
|
<FlowRow
|
||||||
key={flow.id}
|
key={flow.id}
|
||||||
flow={flow}
|
flow={flow}
|
||||||
onDuplicateFlow={fetchFlows}
|
onDuplicateFlow={onDuplicateFlow}
|
||||||
onDeleteFlow={fetchFlows}
|
onDeleteFlow={fetchFlows}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{!isLoading && !hasFlows && (
|
{!isLoading && !navigateToLastPage && !hasFlows && (
|
||||||
<NoResultFound
|
<NoResultFound
|
||||||
text={formatMessage('flows.noFlows')}
|
text={formatMessage('flows.noFlows')}
|
||||||
{...(currentUserAbility.can('create', 'Flow') && {
|
{...(currentUserAbility.can('create', 'Flow') && {
|
||||||
@@ -131,23 +159,23 @@ export default function Flows() {
|
|||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!isLoading && pageInfo && pageInfo.totalPages > 1 && (
|
{!isLoading &&
|
||||||
<Pagination
|
!navigateToLastPage &&
|
||||||
sx={{ display: 'flex', justifyContent: 'center', mt: 3 }}
|
pageInfo &&
|
||||||
page={pageInfo?.currentPage}
|
pageInfo.totalPages > 1 && (
|
||||||
count={pageInfo?.totalPages}
|
<Pagination
|
||||||
onChange={(event, page) =>
|
sx={{ display: 'flex', justifyContent: 'center', mt: 3 }}
|
||||||
setSearchParams({ page: page.toString() })
|
page={pageInfo?.currentPage}
|
||||||
}
|
count={pageInfo?.totalPages}
|
||||||
renderItem={(item) => (
|
renderItem={(item) => (
|
||||||
<PaginationItem
|
<PaginationItem
|
||||||
component={Link}
|
component={Link}
|
||||||
to={`${item.page === 1 ? '' : `?page=${item.page}`}`}
|
to={getPathWithSearchParams(item.page, flowName)}
|
||||||
{...item}
|
{...item}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Container>
|
</Container>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
@@ -266,8 +266,8 @@ function ProfileSettings() {
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid item xs={12} justifyContent="flex-end" sx={{ pt: 5 }}>
|
<Grid item xs={12} justifyContent="flex-end" sx={{ pt: 5 }}>
|
||||||
<Alert variant="outlined" severity="error" sx={{ fontWeight: 500 }}>
|
<Alert variant="outlined" severity="error">
|
||||||
<AlertTitle sx={{ fontWeight: 700 }}>
|
<AlertTitle>
|
||||||
{formatMessage('profileSettings.deleteMyAccount')}
|
{formatMessage('profileSettings.deleteMyAccount')}
|
||||||
</AlertTitle>
|
</AlertTitle>
|
||||||
|
|
||||||
|
@@ -278,6 +278,20 @@ export const defaultTheme = createTheme({
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
MuiAlert: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: ({ theme }) => ({
|
||||||
|
fontWeight: theme.typography.fontWeightRegular,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiAlertTitle: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: ({ theme }) => ({
|
||||||
|
fontWeight: theme.typography.fontWeightBold,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
export const mationTheme = createTheme(
|
export const mationTheme = createTheme(
|
||||||
|
11069
packages/web/yarn.lock
Normal file
11069
packages/web/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user