Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
015d65ac98 chore(deps): bump http-proxy-middleware from 2.0.1 to 2.0.7
Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 2.0.1 to 2.0.7.
- [Release notes](https://github.com/chimurai/http-proxy-middleware/releases)
- [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v2.0.7/CHANGELOG.md)
- [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v2.0.1...v2.0.7)

---
updated-dependencies:
- dependency-name: http-proxy-middleware
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-25 03:27:15 +00:00
224 changed files with 18499 additions and 22998 deletions

View File

@@ -5,11 +5,8 @@ BACKEND_PORT=3000
WEB_PORT=3001 WEB_PORT=3001
echo "Configuring backend environment variables..." echo "Configuring backend environment variables..."
cd packages/backend cd packages/backend
rm -rf .env rm -rf .env
echo " echo "
PORT=$BACKEND_PORT PORT=$BACKEND_PORT
WEB_APP_URL=http://localhost:$WEB_PORT WEB_APP_URL=http://localhost:$WEB_PORT
@@ -24,35 +21,24 @@ WEBHOOK_SECRET_KEY=sample_webhook_secret_key
APP_SECRET_KEY=sample_app_secret_key APP_SECRET_KEY=sample_app_secret_key
REDIS_HOST=redis REDIS_HOST=redis
SERVE_WEB_APP_SEPARATELY=true" >> .env SERVE_WEB_APP_SEPARATELY=true" >> .env
echo "Installing backend dependencies..."
yarn
cd $CURRENT_DIR cd $CURRENT_DIR
echo "Configuring web environment variables..." echo "Configuring web environment variables..."
cd packages/web cd packages/web
rm -rf .env rm -rf .env
echo " echo "
PORT=$WEB_PORT PORT=$WEB_PORT
REACT_APP_BACKEND_URL=http://localhost:$BACKEND_PORT REACT_APP_BACKEND_URL=http://localhost:$BACKEND_PORT
" >> .env " >> .env
echo "Installing web dependencies..."
yarn
cd $CURRENT_DIR cd $CURRENT_DIR
echo "Installing and linking dependencies..."
yarn
yarn lerna bootstrap
echo "Migrating database..." echo "Migrating database..."
cd packages/backend cd packages/backend
yarn db:migrate yarn db:migrate
yarn db:seed:user yarn db:seed:user
echo "Done!" echo "Done!"

View File

@@ -41,11 +41,8 @@ jobs:
with: with:
node-version: 18 node-version: 18
- name: Install dependencies - name: Install dependencies
run: yarn run: cd packages/backend && yarn
working-directory: packages/backend
- name: Copy .env-example.test file to .env.test - name: Copy .env-example.test file to .env.test
run: cp .env-example.test .env.test run: cd packages/backend && cp .env-example.test .env.test
working-directory: packages/backend
- name: Run tests - name: Run tests
run: yarn test:coverage run: cd packages/backend && yarn test
working-directory: packages/backend

View File

@@ -18,13 +18,11 @@ jobs:
with: with:
node-version: '18' node-version: '18'
cache: 'yarn' cache: 'yarn'
cache-dependency-path: packages/backend/yarn.lock cache-dependency-path: yarn.lock
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner." - run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner." - run: echo "🖥️ The workflow is now ready to test your code on the runner."
- run: yarn --frozen-lockfile - run: yarn --frozen-lockfile
working-directory: packages/backend - run: cd packages/backend && yarn lint
- run: yarn lint
working-directory: packages/backend
- run: echo "🍏 This job's status is ${{ job.status }}." - run: echo "🍏 This job's status is ${{ job.status }}."
start-backend-server: start-backend-server:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -37,13 +35,11 @@ jobs:
with: with:
node-version: '18' node-version: '18'
cache: 'yarn' cache: 'yarn'
cache-dependency-path: packages/backend/yarn.lock cache-dependency-path: yarn.lock
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner." - run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner." - run: echo "🖥️ The workflow is now ready to test your code on the runner."
- run: yarn --frozen-lockfile - run: yarn --frozen-lockfile && yarn lerna bootstrap
working-directory: packages/backend - run: cd packages/backend && yarn start
- run: yarn start
working-directory: packages/backend
env: env:
ENCRYPTION_KEY: sample_encryption_key ENCRYPTION_KEY: sample_encryption_key
WEBHOOK_SECRET_KEY: sample_webhook_secret_key WEBHOOK_SECRET_KEY: sample_webhook_secret_key
@@ -59,13 +55,11 @@ jobs:
with: with:
node-version: '18' node-version: '18'
cache: 'yarn' cache: 'yarn'
cache-dependency-path: packages/backend/yarn.lock cache-dependency-path: yarn.lock
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner." - run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner." - run: echo "🖥️ The workflow is now ready to test your code on the runner."
- run: yarn --frozen-lockfile - run: yarn --frozen-lockfile && yarn lerna bootstrap
working-directory: packages/backend - run: cd packages/backend && yarn start:worker
- run: yarn start:worker
working-directory: packages/backend
env: env:
ENCRYPTION_KEY: sample_encryption_key ENCRYPTION_KEY: sample_encryption_key
WEBHOOK_SECRET_KEY: sample_webhook_secret_key WEBHOOK_SECRET_KEY: sample_webhook_secret_key
@@ -81,13 +75,11 @@ jobs:
with: with:
node-version: '18' node-version: '18'
cache: 'yarn' cache: 'yarn'
cache-dependency-path: packages/web/yarn.lock cache-dependency-path: yarn.lock
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner." - run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner." - run: echo "🖥️ The workflow is now ready to test your code on the runner."
- run: yarn --frozen-lockfile - run: yarn --frozen-lockfile && yarn lerna bootstrap
working-directory: packages/web - run: cd packages/web && yarn build
- run: yarn build
working-directory: packages/web
env: env:
CI: false CI: false
- run: echo "🍏 This job's status is ${{ job.status }}." - run: echo "🍏 This job's status is ${{ job.status }}."

View File

@@ -3,7 +3,6 @@ on:
push: push:
branches: branches:
- main - main
# TODO: Add pull request after optimizing the total excecution time of the test suite.
pull_request: pull_request:
paths: paths:
- 'packages/backend/**' - 'packages/backend/**'
@@ -59,21 +58,13 @@ jobs:
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
node-version: 18 node-version: 18
- name: Install web dependencies - name: Install dependencies
run: yarn run: yarn && yarn lerna bootstrap
working-directory: ./packages/web
- name: Install backend dependencies
run: yarn
working-directory: ./packages/backend
- name: Install e2e-tests dependencies
run: yarn
working-directory: ./packages/e2e-tests
- name: Install Playwright Browsers - name: Install Playwright Browsers
run: yarn playwright install --with-deps run: yarn playwright install --with-deps
working-directory: ./packages/e2e-tests
- name: Build Automatisch web - name: Build Automatisch web
run: yarn build
working-directory: ./packages/web working-directory: ./packages/web
run: yarn build
env: env:
# Keep this until we clean up warnings in build processes # Keep this until we clean up warnings in build processes
CI: false CI: false

1
.gitignore vendored
View File

@@ -4,6 +4,7 @@ logs
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
lerna-debug.log*
.pnpm-debug.log* .pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html) # Diagnostic reports (https://nodejs.org/api/report.html)

View File

@@ -11,12 +11,10 @@ WORKDIR /automatisch
# copy the app, note .dockerignore # copy the app, note .dockerignore
COPY . /automatisch COPY . /automatisch
RUN cd packages/web && yarn RUN yarn
RUN cd packages/web && yarn build RUN cd packages/web && yarn build
RUN cd packages/backend && yarn --production
RUN \ RUN \
rm -rf /usr/local/share/.cache/ && \ rm -rf /usr/local/share/.cache/ && \
apk del build-dependencies apk del build-dependencies

13
lerna.json Normal file
View File

@@ -0,0 +1,13 @@
{
"packages": [
"packages/*"
],
"version": "0.10.0",
"npmClient": "yarn",
"useWorkspaces": true,
"command": {
"add": {
"exact": true
}
}
}

32
package.json Normal file
View File

@@ -0,0 +1,32 @@
{
"name": "@automatisch/root",
"license": "See LICENSE file",
"private": true,
"scripts": {
"start": "lerna run --stream --parallel --scope=@*/{web,backend} dev",
"start:web": "lerna run --stream --scope=@*/web dev",
"start:backend": "lerna run --stream --scope=@*/backend dev",
"build:docs": "cd ./packages/docs && yarn install && yarn build"
},
"workspaces": {
"packages": [
"packages/*"
],
"nohoist": [
"**/babel-loader",
"**/webpack",
"**/@automatisch/web",
"**/ajv"
]
},
"devDependencies": {
"eslint": "^8.13.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"lerna": "^4.0.0",
"prettier": "^2.5.1"
},
"publishConfig": {
"access": "public"
}
}

View File

@@ -12,7 +12,6 @@
"pretest": "APP_ENV=test node ./test/setup/prepare-test-env.js", "pretest": "APP_ENV=test node ./test/setup/prepare-test-env.js",
"test": "APP_ENV=test vitest run", "test": "APP_ENV=test vitest run",
"test:watch": "APP_ENV=test vitest watch", "test:watch": "APP_ENV=test vitest watch",
"test:coverage": "yarn test --coverage",
"lint": "eslint .", "lint": "eslint .",
"db:create": "node ./bin/database/create.js", "db:create": "node ./bin/database/create.js",
"db:seed:user": "node ./bin/database/seed-user.js", "db:seed:user": "node ./bin/database/seed-user.js",
@@ -24,7 +23,6 @@
"dependencies": { "dependencies": {
"@bull-board/express": "^3.10.1", "@bull-board/express": "^3.10.1",
"@casl/ability": "^6.5.0", "@casl/ability": "^6.5.0",
"@faker-js/faker": "^9.2.0",
"@node-saml/passport-saml": "^4.0.4", "@node-saml/passport-saml": "^4.0.4",
"@rudderstack/rudder-sdk-node": "^1.1.2", "@rudderstack/rudder-sdk-node": "^1.1.2",
"@sentry/node": "^7.42.0", "@sentry/node": "^7.42.0",
@@ -38,9 +36,6 @@
"crypto-js": "^4.1.1", "crypto-js": "^4.1.1",
"debug": "~2.6.9", "debug": "~2.6.9",
"dotenv": "^10.0.0", "dotenv": "^10.0.0",
"eslint": "^8.13.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"express": "~4.18.2", "express": "~4.18.2",
"express-async-errors": "^3.1.1", "express-async-errors": "^3.1.1",
"express-basic-auth": "^1.2.1", "express-basic-auth": "^1.2.1",
@@ -66,7 +61,6 @@
"pg": "^8.7.1", "pg": "^8.7.1",
"php-serialize": "^4.0.2", "php-serialize": "^4.0.2",
"pluralize": "^8.0.0", "pluralize": "^8.0.0",
"prettier": "^2.5.1",
"raw-body": "^2.5.2", "raw-body": "^2.5.2",
"showdown": "^2.1.0", "showdown": "^2.1.0",
"uuid": "^9.0.1", "uuid": "^9.0.1",
@@ -98,11 +92,10 @@
"url": "https://github.com/automatisch/automatisch/issues" "url": "https://github.com/automatisch/automatisch/issues"
}, },
"devDependencies": { "devDependencies": {
"@vitest/coverage-v8": "^2.1.5",
"node-gyp": "^10.1.0", "node-gyp": "^10.1.0",
"nodemon": "^2.0.13", "nodemon": "^2.0.13",
"supertest": "^6.3.3", "supertest": "^6.3.3",
"vitest": "^2.1.5" "vitest": "^1.1.3"
}, },
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"

View File

@@ -8,7 +8,7 @@ export default {
key: 'instanceUrl', key: 'instanceUrl',
label: 'WordPress instance URL', label: 'WordPress instance URL',
type: 'string', type: 'string',
required: true, required: false,
readOnly: false, readOnly: false,
value: null, value: null,
placeholder: null, placeholder: null,

View File

@@ -52,7 +52,7 @@ const appConfig = {
isDev: appEnv === 'development', isDev: appEnv === 'development',
isTest: appEnv === 'test', isTest: appEnv === 'test',
isProd: appEnv === 'production', isProd: appEnv === 'production',
version: '0.14.0', version: '0.13.1',
postgresDatabase: process.env.POSTGRES_DATABASE || 'automatisch_development', postgresDatabase: process.env.POSTGRES_DATABASE || 'automatisch_development',
postgresSchema: process.env.POSTGRES_SCHEMA || 'public', postgresSchema: process.env.POSTGRES_SCHEMA || 'public',
postgresPort: parseInt(process.env.POSTGRES_PORT || '5432'), postgresPort: parseInt(process.env.POSTGRES_PORT || '5432'),

View File

@@ -32,7 +32,7 @@ describe('POST /api/v1/access-tokens', () => {
}) })
.expect(422); .expect(422);
expect(response.body.errors.general).toStrictEqual([ expect(response.body.errors.general).toEqual([
'Incorrect email or password.', 'Incorrect email or password.',
]); ]);
}); });

View File

@@ -83,7 +83,7 @@ describe('POST /api/v1/admin/apps/:appKey/auth-clients', () => {
.send(appAuthClient) .send(appAuthClient)
.expect(422); .expect(422);
expect(response.body.meta.type).toStrictEqual('ModelValidation'); expect(response.body.meta.type).toEqual('ModelValidation');
expect(response.body.errors).toMatchObject({ expect(response.body.errors).toMatchObject({
name: ["must have required property 'name'"], name: ["must have required property 'name'"],
formattedAuthDefaults: [ formattedAuthDefaults: [

View File

@@ -10,11 +10,12 @@ export default async (request, response) => {
}; };
const appConfigParams = (request) => { const appConfigParams = (request) => {
const { useOnlyPredefinedAuthClients, disabled } = request.body; const { customConnectionAllowed, shared, disabled } = request.body;
return { return {
key: request.params.appKey, key: request.params.appKey,
useOnlyPredefinedAuthClients, customConnectionAllowed,
shared,
disabled, disabled,
}; };
}; };

View File

@@ -23,7 +23,8 @@ describe('POST /api/v1/admin/apps/:appKey/config', () => {
it('should return created app config', async () => { it('should return created app config', async () => {
const appConfig = { const appConfig = {
useOnlyPredefinedAuthClients: false, customConnectionAllowed: true,
shared: true,
disabled: false, disabled: false,
}; };
@@ -37,14 +38,14 @@ describe('POST /api/v1/admin/apps/:appKey/config', () => {
...appConfig, ...appConfig,
key: 'gitlab', key: 'gitlab',
}); });
expect(response.body).toMatchObject(expectedPayload); expect(response.body).toMatchObject(expectedPayload);
}); });
it('should return HTTP 422 for already existing app config', async () => { it('should return HTTP 422 for already existing app config', async () => {
const appConfig = { const appConfig = {
key: 'gitlab', key: 'gitlab',
useOnlyPredefinedAuthClients: false, customConnectionAllowed: true,
shared: true,
disabled: false, disabled: false,
}; };
@@ -58,7 +59,7 @@ describe('POST /api/v1/admin/apps/:appKey/config', () => {
}) })
.expect(422); .expect(422);
expect(response.body.meta.type).toStrictEqual('UniqueViolationError'); expect(response.body.meta.type).toEqual('UniqueViolationError');
expect(response.body.errors).toMatchObject({ expect(response.body.errors).toMatchObject({
key: ["'key' must be unique."], key: ["'key' must be unique."],
}); });

View File

@@ -32,7 +32,7 @@ describe('GET /api/v1/admin/apps/:appKey/auth-clients/:appAuthClientId', () => {
.expect(200); .expect(200);
const expectedPayload = getAppAuthClientMock(currentAppAuthClient); const expectedPayload = getAppAuthClientMock(currentAppAuthClient);
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
it('should return not found response for not existing app auth client ID', async () => { it('should return not found response for not existing app auth client ID', async () => {

View File

@@ -39,6 +39,6 @@ describe('GET /api/v1/admin/apps/:appKey/auth-clients', () => {
appAuthClientOne, appAuthClientOne,
]); ]);
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
}); });

View File

@@ -17,10 +17,11 @@ export default async (request, response) => {
}; };
const appConfigParams = (request) => { const appConfigParams = (request) => {
const { useOnlyPredefinedAuthClients, disabled } = request.body; const { customConnectionAllowed, shared, disabled } = request.body;
return { return {
useOnlyPredefinedAuthClients, customConnectionAllowed,
shared,
disabled, disabled,
}; };
}; };

View File

@@ -24,15 +24,17 @@ describe('PATCH /api/v1/admin/apps/:appKey/config', () => {
it('should return updated app config', async () => { it('should return updated app config', async () => {
const appConfig = { const appConfig = {
key: 'gitlab', key: 'gitlab',
useOnlyPredefinedAuthClients: true, customConnectionAllowed: true,
shared: true,
disabled: false, disabled: false,
}; };
await createAppConfig(appConfig); await createAppConfig(appConfig);
const newAppConfigValues = { const newAppConfigValues = {
shared: false,
disabled: true, disabled: true,
useOnlyPredefinedAuthClients: false, customConnectionAllowed: false,
}; };
const response = await request(app) const response = await request(app)
@@ -51,8 +53,9 @@ describe('PATCH /api/v1/admin/apps/:appKey/config', () => {
it('should return not found response for unexisting app config', async () => { it('should return not found response for unexisting app config', async () => {
const appConfig = { const appConfig = {
shared: false,
disabled: true, disabled: true,
useOnlyPredefinedAuthClients: false, customConnectionAllowed: false,
}; };
await request(app) await request(app)
@@ -65,7 +68,8 @@ describe('PATCH /api/v1/admin/apps/:appKey/config', () => {
it('should return HTTP 422 for invalid app config data', async () => { it('should return HTTP 422 for invalid app config data', async () => {
const appConfig = { const appConfig = {
key: 'gitlab', key: 'gitlab',
useOnlyPredefinedAuthClients: true, customConnectionAllowed: true,
shared: true,
disabled: false, disabled: false,
}; };
@@ -79,7 +83,7 @@ describe('PATCH /api/v1/admin/apps/:appKey/config', () => {
}) })
.expect(422); .expect(422);
expect(response.body.meta.type).toStrictEqual('ModelValidation'); expect(response.body.meta.type).toEqual('ModelValidation');
expect(response.body.errors).toMatchObject({ expect(response.body.errors).toMatchObject({
disabled: ['must be boolean'], disabled: ['must be boolean'],
}); });

View File

@@ -50,8 +50,8 @@ describe('PATCH /api/v1/admin/config', () => {
.send(newConfigValues) .send(newConfigValues)
.expect(200); .expect(200);
expect(response.body.data.title).toStrictEqual(newTitle); expect(response.body.data.title).toEqual(newTitle);
expect(response.body.meta.type).toStrictEqual('Config'); expect(response.body.meta.type).toEqual('Config');
}); });
it('should return created config for unexisting config', async () => { it('should return created config for unexisting config', async () => {
@@ -67,8 +67,8 @@ describe('PATCH /api/v1/admin/config', () => {
.send(newConfigValues) .send(newConfigValues)
.expect(200); .expect(200);
expect(response.body.data.title).toStrictEqual(newTitle); expect(response.body.data.title).toEqual(newTitle);
expect(response.body.meta.type).toStrictEqual('Config'); expect(response.body.meta.type).toEqual('Config');
}); });
it('should return null for deleted config entry', async () => { it('should return null for deleted config entry', async () => {
@@ -83,6 +83,6 @@ describe('PATCH /api/v1/admin/config', () => {
.expect(200); .expect(200);
expect(response.body.data.title).toBeNull(); expect(response.body.data.title).toBeNull();
expect(response.body.meta.type).toStrictEqual('Config'); expect(response.body.meta.type).toEqual('Config');
}); });
}); });

View File

@@ -27,6 +27,6 @@ describe('GET /api/v1/admin/permissions/catalog', () => {
const expectedPayload = await getPermissionsCatalogMock(); const expectedPayload = await getPermissionsCatalogMock();
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
}); });

View File

@@ -58,7 +58,7 @@ describe('POST /api/v1/admin/roles', () => {
] ]
); );
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
it('should return unprocessable entity response for invalid role data', async () => { it('should return unprocessable entity response for invalid role data', async () => {

View File

@@ -92,4 +92,21 @@ 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);
});
}); });

View File

@@ -34,7 +34,7 @@ describe('GET /api/v1/admin/roles/:roleId', () => {
permissionTwo, permissionTwo,
]); ]);
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
it('should return not found response for not existing role UUID', async () => { it('should return not found response for not existing role UUID', async () => {

View File

@@ -28,6 +28,6 @@ describe('GET /api/v1/admin/roles', () => {
const expectedPayload = await getRolesMock([roleOne, roleTwo]); const expectedPayload = await getRolesMock([roleOne, roleTwo]);
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
}); });

View File

@@ -7,7 +7,7 @@ export default async (request, response) => {
.throwIfNotFound(); .throwIfNotFound();
const roleMappings = await samlAuthProvider const roleMappings = await samlAuthProvider
.$relatedQuery('roleMappings') .$relatedQuery('samlAuthProvidersRoleMappings')
.orderBy('remote_role_name', 'asc'); .orderBy('remote_role_name', 'asc');
renderObject(response, roleMappings); renderObject(response, roleMappings);

View File

@@ -46,6 +46,6 @@ describe('GET /api/v1/admin/saml-auth-providers/:samlAuthProviderId/role-mapping
roleMappingTwo, roleMappingTwo,
]); ]);
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
}); });

View File

@@ -30,7 +30,7 @@ describe('GET /api/v1/admin/saml-auth-provider/:samlAuthProviderId', () => {
const expectedPayload = await getSamlAuthProviderMock(samlAuthProvider); const expectedPayload = await getSamlAuthProviderMock(samlAuthProvider);
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
it('should return not found response for not existing saml auth provider UUID', async () => { it('should return not found response for not existing saml auth provider UUID', async () => {

View File

@@ -34,6 +34,6 @@ describe('GET /api/v1/admin/saml-auth-providers', () => {
samlAuthProviderOne, samlAuthProviderOne,
]); ]);
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
}); });

View File

@@ -8,14 +8,15 @@ export default async (request, response) => {
.findById(samlAuthProviderId) .findById(samlAuthProviderId)
.throwIfNotFound(); .throwIfNotFound();
const roleMappings = await samlAuthProvider.updateRoleMappings( const samlAuthProvidersRoleMappings =
roleMappingsParams(request) await samlAuthProvider.updateRoleMappings(
); samlAuthProvidersRoleMappingsParams(request)
);
renderObject(response, roleMappings); renderObject(response, samlAuthProvidersRoleMappings);
}; };
const roleMappingsParams = (request) => { const samlAuthProvidersRoleMappingsParams = (request) => {
const roleMappings = request.body; const roleMappings = request.body;
return roleMappings.map(({ roleId, remoteRoleName }) => ({ return roleMappings.map(({ roleId, remoteRoleName }) => ({

View File

@@ -6,7 +6,7 @@ import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by
import { createRole } from '../../../../../../test/factories/role.js'; import { createRole } from '../../../../../../test/factories/role.js';
import { createUser } from '../../../../../../test/factories/user.js'; import { createUser } from '../../../../../../test/factories/user.js';
import { createSamlAuthProvider } from '../../../../../../test/factories/saml-auth-provider.ee.js'; import { createSamlAuthProvider } from '../../../../../../test/factories/saml-auth-provider.ee.js';
import { createRoleMapping } from '../../../../../../test/factories/role-mapping.js'; import { createSamlAuthProvidersRoleMapping } from '../../../../../../test/factories/saml-auth-providers-role-mapping.js';
import createRoleMappingsMock from '../../../../../../test/mocks/rest/api/v1/admin/saml-auth-providers/update-role-mappings.ee.js'; import createRoleMappingsMock from '../../../../../../test/mocks/rest/api/v1/admin/saml-auth-providers/update-role-mappings.ee.js';
import * as license from '../../../../../helpers/license.ee.js'; import * as license from '../../../../../helpers/license.ee.js';
@@ -21,12 +21,12 @@ describe('PATCH /api/v1/admin/saml-auth-providers/:samlAuthProviderId/role-mappi
samlAuthProvider = await createSamlAuthProvider(); samlAuthProvider = await createSamlAuthProvider();
await createRoleMapping({ await createSamlAuthProvidersRoleMapping({
samlAuthProviderId: samlAuthProvider.id, samlAuthProviderId: samlAuthProvider.id,
remoteRoleName: 'Viewer', remoteRoleName: 'Viewer',
}); });
await createRoleMapping({ await createSamlAuthProvidersRoleMapping({
samlAuthProviderId: samlAuthProvider.id, samlAuthProviderId: samlAuthProvider.id,
remoteRoleName: 'Editor', remoteRoleName: 'Editor',
}); });
@@ -64,7 +64,7 @@ describe('PATCH /api/v1/admin/saml-auth-providers/:samlAuthProviderId/role-mappi
it('should delete role mappings when given empty role mappings', async () => { it('should delete role mappings when given empty role mappings', async () => {
const existingRoleMappings = await samlAuthProvider.$relatedQuery( const existingRoleMappings = await samlAuthProvider.$relatedQuery(
'roleMappings' 'samlAuthProvidersRoleMappings'
); );
expect(existingRoleMappings.length).toBe(2); expect(existingRoleMappings.length).toBe(2);
@@ -149,4 +149,34 @@ describe('PATCH /api/v1/admin/saml-auth-providers/:samlAuthProviderId/role-mappi
.send(roleMappings) .send(roleMappings)
.expect(404); .expect(404);
}); });
it('should not delete existing role mapping when error thrown', async () => {
const roleMappings = [
{
roleId: userRole.id,
remoteRoleName: {
invalid: 'data',
},
},
];
const roleMappingsBeforeRequest = await samlAuthProvider.$relatedQuery(
'samlAuthProvidersRoleMappings'
);
await request(app)
.patch(
`/api/v1/admin/saml-auth-providers/${samlAuthProvider.id}/role-mappings`
)
.set('Authorization', token)
.send(roleMappings)
.expect(422);
const roleMappingsAfterRequest = await samlAuthProvider.$relatedQuery(
'samlAuthProvidersRoleMappings'
);
expect(roleMappingsBeforeRequest).toStrictEqual(roleMappingsAfterRequest);
expect(roleMappingsAfterRequest.length).toBe(2);
});
}); });

View File

@@ -30,7 +30,7 @@ describe('GET /api/v1/admin/users/:userId', () => {
.expect(200); .expect(200);
const expectedPayload = getUserMock(anotherUser, anotherUserRole); const expectedPayload = getUserMock(anotherUser, anotherUserRole);
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
it('should return not found response for not existing user UUID', async () => { it('should return not found response for not existing user UUID', async () => {

View File

@@ -40,6 +40,6 @@ describe('GET /api/v1/admin/users', () => {
[anotherUserRole, currentUserRole] [anotherUserRole, currentUserRole]
); );
expect(response.body).toStrictEqual(expectedResponsePayload); expect(response.body).toEqual(expectedResponsePayload);
}); });
}); });

View File

@@ -61,8 +61,7 @@ describe('PATCH /api/v1/admin/users/:userId', () => {
.send(anotherUserUpdatedData) .send(anotherUserUpdatedData)
.expect(422); .expect(422);
expect(response.body.meta.type).toStrictEqual('ModelValidation'); expect(response.body.meta.type).toEqual('ModelValidation');
expect(response.body.errors).toMatchObject({ expect(response.body.errors).toMatchObject({
email: ['must be string'], email: ['must be string'],
fullName: ['must be string'], fullName: ['must be string'],

View File

@@ -155,7 +155,7 @@ describe('POST /api/v1/apps/:appKey/connections', () => {
await createAppConfig({ await createAppConfig({
key: 'gitlab', key: 'gitlab',
disabled: false, disabled: false,
useOnlyPredefinedAuthClients: false, customConnectionAllowed: true,
}); });
}); });
@@ -218,7 +218,7 @@ describe('POST /api/v1/apps/:appKey/connections', () => {
await createAppConfig({ await createAppConfig({
key: 'gitlab', key: 'gitlab',
disabled: false, disabled: false,
useOnlyPredefinedAuthClients: true, customConnectionAllowed: false,
}); });
}); });
@@ -266,14 +266,14 @@ describe('POST /api/v1/apps/:appKey/connections', () => {
}); });
}); });
describe('with auth client enabled', async () => { describe('with auth clients enabled', async () => {
let appAuthClient; let appAuthClient;
beforeEach(async () => { beforeEach(async () => {
await createAppConfig({ await createAppConfig({
key: 'gitlab', key: 'gitlab',
disabled: false, disabled: false,
useOnlyPredefinedAuthClients: false, shared: true,
}); });
appAuthClient = await createAppAuthClient({ appAuthClient = await createAppAuthClient({
@@ -310,6 +310,19 @@ describe('POST /api/v1/apps/:appKey/connections', () => {
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toStrictEqual(expectedPayload);
}); });
it('should return not authorized response for appAuthClientId and formattedData together', async () => {
const connectionData = {
appAuthClientId: appAuthClient.id,
formattedData: {},
};
await request(app)
.post('/api/v1/apps/gitlab/connections')
.set('Authorization', token)
.send(connectionData)
.expect(403);
});
it('should return not found response for invalid app key', async () => { it('should return not found response for invalid app key', async () => {
await request(app) await request(app)
.post('/api/v1/apps/invalid-app-key/connections') .post('/api/v1/apps/invalid-app-key/connections')
@@ -336,20 +349,18 @@ describe('POST /api/v1/apps/:appKey/connections', () => {
}); });
}); });
}); });
describe('with auth clients disabled', async () => {
describe('with auth client disabled', async () => {
let appAuthClient; let appAuthClient;
beforeEach(async () => { beforeEach(async () => {
await createAppConfig({ await createAppConfig({
key: 'gitlab', key: 'gitlab',
disabled: false, disabled: false,
useOnlyPredefinedAuthClients: false, shared: false,
}); });
appAuthClient = await createAppAuthClient({ appAuthClient = await createAppAuthClient({
appKey: 'gitlab', appKey: 'gitlab',
active: false,
}); });
}); });
@@ -362,7 +373,7 @@ describe('POST /api/v1/apps/:appKey/connections', () => {
.post('/api/v1/apps/gitlab/connections') .post('/api/v1/apps/gitlab/connections')
.set('Authorization', token) .set('Authorization', token)
.send(connectionData) .send(connectionData)
.expect(404); .expect(403);
}); });
it('should return not found response for invalid app key', async () => { it('should return not found response for invalid app key', async () => {

View File

@@ -29,7 +29,7 @@ describe('GET /api/v1/apps/:appKey/actions/:actionKey/substeps', () => {
.expect(200); .expect(200);
const expectedPayload = getActionSubstepsMock(exampleAction.substeps); const expectedPayload = getActionSubstepsMock(exampleAction.substeps);
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
it('should return not found response for invalid app key', async () => { it('should return not found response for invalid app key', async () => {
@@ -47,6 +47,6 @@ describe('GET /api/v1/apps/:appKey/actions/:actionKey/substeps', () => {
.set('Authorization', token) .set('Authorization', token)
.expect(200); .expect(200);
expect(response.body.data).toStrictEqual([]); expect(response.body.data).toEqual([]);
}); });
}); });

View File

@@ -23,7 +23,7 @@ describe('GET /api/v1/apps/:appKey/actions', () => {
.expect(200); .expect(200);
const expectedPayload = getActionsMock(exampleApp.actions); const expectedPayload = getActionsMock(exampleApp.actions);
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
it('should return not found response for invalid app key', async () => { it('should return not found response for invalid app key', async () => {

View File

@@ -23,7 +23,7 @@ describe('GET /api/v1/apps/:appKey', () => {
.expect(200); .expect(200);
const expectedPayload = getAppMock(exampleApp); const expectedPayload = getAppMock(exampleApp);
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
it('should return not found response for invalid app key', async () => { it('should return not found response for invalid app key', async () => {

View File

@@ -22,7 +22,7 @@ describe('GET /api/v1/apps', () => {
.expect(200); .expect(200);
const expectedPayload = getAppsMock(apps); const expectedPayload = getAppsMock(apps);
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
it('should return all apps filtered by name', async () => { it('should return all apps filtered by name', async () => {
@@ -34,7 +34,7 @@ describe('GET /api/v1/apps', () => {
.expect(200); .expect(200);
const expectedPayload = getAppsMock(appsWithNameGit); const expectedPayload = getAppsMock(appsWithNameGit);
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
it('should return only the apps with triggers', async () => { it('should return only the apps with triggers', async () => {
@@ -46,7 +46,7 @@ describe('GET /api/v1/apps', () => {
.expect(200); .expect(200);
const expectedPayload = getAppsMock(appsWithTriggers); const expectedPayload = getAppsMock(appsWithTriggers);
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
it('should return only the apps with actions', async () => { it('should return only the apps with actions', async () => {
@@ -58,6 +58,6 @@ describe('GET /api/v1/apps', () => {
.expect(200); .expect(200);
const expectedPayload = getAppsMock(appsWithActions); const expectedPayload = getAppsMock(appsWithActions);
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
}); });

View File

@@ -29,7 +29,7 @@ describe('GET /api/v1/apps/:appKey/auth-clients/:appAuthClientId', () => {
.expect(200); .expect(200);
const expectedPayload = getAppAuthClientMock(currentAppAuthClient); const expectedPayload = getAppAuthClientMock(currentAppAuthClient);
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
it('should return not found response for not existing app auth client ID', async () => { it('should return not found response for not existing app auth client ID', async () => {

View File

@@ -37,6 +37,6 @@ describe('GET /api/v1/apps/:appKey/auth-clients', () => {
appAuthClientOne, appAuthClientOne,
]); ]);
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
}); });

View File

@@ -23,7 +23,7 @@ describe('GET /api/v1/apps/:appKey/auth', () => {
.expect(200); .expect(200);
const expectedPayload = getAuthMock(exampleApp.auth); const expectedPayload = getAuthMock(exampleApp.auth);
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
it('should return not found response for invalid app key', async () => { it('should return not found response for invalid app key', async () => {

View File

@@ -17,7 +17,8 @@ describe('GET /api/v1/apps/:appKey/config', () => {
appConfig = await createAppConfig({ appConfig = await createAppConfig({
key: 'deepl', key: 'deepl',
useOnlyPredefinedAuthClients: false, customConnectionAllowed: true,
shared: true,
disabled: false, disabled: false,
}); });
@@ -31,7 +32,7 @@ describe('GET /api/v1/apps/:appKey/config', () => {
.expect(200); .expect(200);
const expectedPayload = getAppConfigMock(appConfig); const expectedPayload = getAppConfigMock(appConfig);
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
it('should return not found response for not existing app key', async () => { it('should return not found response for not existing app key', async () => {

View File

@@ -47,7 +47,7 @@ describe('GET /api/v1/apps/:appKey/connections', () => {
currentUserConnectionOne, currentUserConnectionOne,
]); ]);
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
it('should return the connections data of specified app for another user', async () => { it('should return the connections data of specified app for another user', async () => {
@@ -82,19 +82,19 @@ describe('GET /api/v1/apps/:appKey/connections', () => {
anotherUserConnectionOne, anotherUserConnectionOne,
]); ]);
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
it('should return not found response for invalid connection UUID', async () => { it('should return not found response for invalid connection UUID', async () => {
await createPermission({ await createPermission({
action: 'read', action: 'update',
subject: 'Connection', subject: 'Connection',
roleId: currentUserRole.id, roleId: currentUserRole.id,
conditions: ['isCreator'], conditions: ['isCreator'],
}); });
await request(app) await request(app)
.get('/api/v1/apps/invalid-connection-id/connections') .get('/api/v1/connections/invalid-connection-id/connections')
.set('Authorization', token) .set('Authorization', token)
.expect(404); .expect(404);
}); });

View File

@@ -62,7 +62,7 @@ describe('GET /api/v1/apps/:appKey/flows', () => {
[triggerStepFlowOne, actionStepFlowOne] [triggerStepFlowOne, actionStepFlowOne]
); );
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
it('should return the flows data of specified app for another user', async () => { it('should return the flows data of specified app for another user', async () => {
@@ -110,7 +110,7 @@ describe('GET /api/v1/apps/:appKey/flows', () => {
[triggerStepFlowOne, actionStepFlowOne] [triggerStepFlowOne, actionStepFlowOne]
); );
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
it('should return not found response for invalid app key', async () => { it('should return not found response for invalid app key', async () => {

View File

@@ -29,7 +29,7 @@ describe('GET /api/v1/apps/:appKey/triggers/:triggerKey/substeps', () => {
.expect(200); .expect(200);
const expectedPayload = getTriggerSubstepsMock(exampleTrigger.substeps); const expectedPayload = getTriggerSubstepsMock(exampleTrigger.substeps);
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
it('should return not found response for invalid app key', async () => { it('should return not found response for invalid app key', async () => {
@@ -47,6 +47,6 @@ describe('GET /api/v1/apps/:appKey/triggers/:triggerKey/substeps', () => {
.set('Authorization', token) .set('Authorization', token)
.expect(200); .expect(200);
expect(response.body.data).toStrictEqual([]); expect(response.body.data).toEqual([]);
}); });
}); });

View File

@@ -23,7 +23,7 @@ describe('GET /api/v1/apps/:appKey/triggers', () => {
.expect(200); .expect(200);
const expectedPayload = getTriggersMock(exampleApp.triggers); const expectedPayload = getTriggersMock(exampleApp.triggers);
expect(expectedPayload).toMatchObject(response.body); expect(response.body).toEqual(expectedPayload);
}); });
it('should return not found response for invalid app key', async () => { it('should return not found response for invalid app key', async () => {

View File

@@ -20,6 +20,6 @@ describe('GET /api/v1/automatisch/info', () => {
const expectedPayload = infoMock(); const expectedPayload = infoMock();
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
}); });

View File

@@ -18,6 +18,6 @@ describe('GET /api/v1/automatisch/license', () => {
const expectedPayload = licenseMock(); const expectedPayload = licenseMock();
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
}); });

View File

@@ -10,7 +10,7 @@ describe('GET /api/v1/automatisch/version', () => {
const expectedPayload = { const expectedPayload = {
data: { data: {
version: '0.14.0', version: '0.13.1',
}, },
meta: { meta: {
count: 1, count: 1,
@@ -21,6 +21,6 @@ describe('GET /api/v1/automatisch/version', () => {
}, },
}; };
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
}); });

View File

@@ -69,7 +69,7 @@ describe('GET /api/v1/connections/:connectionId/flows', () => {
[triggerStepFlowOne, actionStepFlowOne] [triggerStepFlowOne, actionStepFlowOne]
); );
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
it('should return the flows data of specified connection for another user', async () => { it('should return the flows data of specified connection for another user', async () => {
@@ -123,6 +123,6 @@ describe('GET /api/v1/connections/:connectionId/flows', () => {
[triggerStepFlowOne, actionStepFlowOne] [triggerStepFlowOne, actionStepFlowOne]
); );
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
}); });

View File

@@ -47,6 +47,7 @@ describe('POST /api/v1/connections/:connectionId/reset', () => {
const expectedPayload = resetConnectionMock({ const expectedPayload = resetConnectionMock({
...refetchedCurrentUserConnection, ...refetchedCurrentUserConnection,
reconnectable: refetchedCurrentUserConnection.reconnectable,
formattedData: { formattedData: {
screenName: 'Connection name', screenName: 'Connection name',
}, },

View File

@@ -43,7 +43,7 @@ describe('POST /api/v1/connections/:connectionId/test', () => {
.set('Authorization', token) .set('Authorization', token)
.expect(200); .expect(200);
expect(response.body.data.verified).toStrictEqual(false); expect(response.body.data.verified).toEqual(false);
}); });
it('should update the connection as not verified for another user', async () => { it('should update the connection as not verified for another user', async () => {
@@ -74,7 +74,7 @@ describe('POST /api/v1/connections/:connectionId/test', () => {
.set('Authorization', token) .set('Authorization', token)
.expect(200); .expect(200);
expect(response.body.data.verified).toStrictEqual(false); expect(response.body.data.verified).toEqual(false);
}); });
it('should return not found response for not existing connection UUID', async () => { it('should return not found response for not existing connection UUID', async () => {

View File

@@ -55,9 +55,10 @@ describe('PATCH /api/v1/connections/:connectionId', () => {
const refetchedCurrentUserConnection = await currentUserConnection.$query(); const refetchedCurrentUserConnection = await currentUserConnection.$query();
const expectedPayload = updateConnectionMock( const expectedPayload = updateConnectionMock({
refetchedCurrentUserConnection ...refetchedCurrentUserConnection,
); reconnectable: refetchedCurrentUserConnection.reconnectable,
});
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toStrictEqual(expectedPayload);
}); });

View File

@@ -47,7 +47,7 @@ describe('POST /api/v1/connections/:connectionId/verify', () => {
.set('Authorization', token) .set('Authorization', token)
.expect(200); .expect(200);
expect(response.body.data.verified).toStrictEqual(true); expect(response.body.data.verified).toEqual(true);
}); });
it('should return not found response for not existing connection UUID', async () => { it('should return not found response for not existing connection UUID', async () => {

View File

@@ -69,7 +69,7 @@ describe('GET /api/v1/executions/:executionId/execution-steps', () => {
[stepOne, stepTwo] [stepOne, stepTwo]
); );
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
it('should return the execution steps of another user execution', async () => { it('should return the execution steps of another user execution', async () => {
@@ -118,7 +118,7 @@ describe('GET /api/v1/executions/:executionId/execution-steps', () => {
[stepOne, stepTwo] [stepOne, stepTwo]
); );
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
it('should return not found response for not existing execution step UUID', async () => { it('should return not found response for not existing execution step UUID', async () => {

View File

@@ -57,7 +57,7 @@ describe('GET /api/v1/executions/:executionId', () => {
[stepOne, stepTwo] [stepOne, stepTwo]
); );
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
it('should return the execution data of another user', async () => { it('should return the execution data of another user', async () => {
@@ -99,7 +99,7 @@ describe('GET /api/v1/executions/:executionId', () => {
[stepOne, stepTwo] [stepOne, stepTwo]
); );
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
it('should return not found response for not existing execution UUID', async () => { it('should return not found response for not existing execution UUID', async () => {

View File

@@ -66,7 +66,7 @@ describe('GET /api/v1/executions', () => {
[stepOne, stepTwo] [stepOne, stepTwo]
); );
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
it('should return the executions of another user', async () => { it('should return the executions of another user', async () => {
@@ -114,6 +114,6 @@ describe('GET /api/v1/executions', () => {
[stepOne, stepTwo] [stepOne, stepTwo]
); );
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
}); });

View File

@@ -1,11 +1,11 @@
import { renderObject } from '../../../../helpers/renderer.js'; import { renderObject } from '../../../../helpers/renderer.js';
export default async (request, response) => { export default async (request, response) => {
const flow = await request.currentUser.$relatedQuery('flows').insertAndFetch({ let flow = await request.currentUser.$relatedQuery('flows').insert({
name: 'Name your flow', name: 'Name your flow',
}); });
await flow.createInitialSteps(); flow = await flow.createInitialSteps();
renderObject(response, flow, { status: 201 }); renderObject(response, flow, { status: 201 });
}; };

View File

@@ -6,7 +6,7 @@ export default async (request, response) => {
.findById(request.params.flowId) .findById(request.params.flowId)
.throwIfNotFound(); .throwIfNotFound();
const createdActionStep = await flow.createStepAfter( const createdActionStep = await flow.createActionStep(
request.body.previousStepId request.body.previousStepId
); );

View File

@@ -41,7 +41,7 @@ describe('GET /api/v1/flows/:flowId', () => {
actionStep, actionStep,
]); ]);
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
it('should return the flow data of another user', async () => { it('should return the flow data of another user', async () => {
@@ -67,7 +67,7 @@ describe('GET /api/v1/flows/:flowId', () => {
actionStep, actionStep,
]); ]);
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
it('should return not found response for not existing flow UUID', async () => { it('should return not found response for not existing flow UUID', async () => {

View File

@@ -63,7 +63,7 @@ describe('GET /api/v1/flows', () => {
] ]
); );
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
it('should return the flows data of another user', async () => { it('should return the flows data of another user', async () => {
@@ -113,6 +113,6 @@ describe('GET /api/v1/flows', () => {
] ]
); );
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
}); });

View File

@@ -53,7 +53,7 @@ describe('POST /api/v1/installation/users', () => {
const usersCountAfter = await User.query().resultSize(); const usersCountAfter = await User.query().resultSize();
expect(usersCountBefore).toStrictEqual(usersCountAfter); expect(usersCountBefore).toEqual(usersCountAfter);
}); });
}); });

View File

@@ -28,6 +28,6 @@ describe('GET /api/v1/payment/paddle-info', () => {
const expectedResponsePayload = await getPaddleInfoMock(); const expectedResponsePayload = await getPaddleInfoMock();
expect(response.body).toStrictEqual(expectedResponsePayload); expect(response.body).toEqual(expectedResponsePayload);
}); });
}); });

View File

@@ -24,6 +24,6 @@ describe('GET /api/v1/payment/plans', () => {
const expectedResponsePayload = await getPaymentPlansMock(); const expectedResponsePayload = await getPaymentPlansMock();
expect(response.body).toStrictEqual(expectedResponsePayload); expect(response.body).toEqual(expectedResponsePayload);
}); });
}); });

View File

@@ -25,6 +25,6 @@ describe('GET /api/v1/saml-auth-providers', () => {
samlAuthProviderOne, samlAuthProviderOne,
]); ]);
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
}); });

View File

@@ -78,7 +78,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-data', () => {
}) })
.expect(200); .expect(200);
expect(response.body.data).toStrictEqual(repositories); expect(response.body.data).toEqual(repositories);
}); });
it('of the another users step', async () => { it('of the another users step', async () => {
@@ -117,7 +117,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-data', () => {
}) })
.expect(200); .expect(200);
expect(response.body.data).toStrictEqual(repositories); expect(response.body.data).toEqual(repositories);
}); });
}); });
@@ -171,7 +171,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-data', () => {
}) })
.expect(422); .expect(422);
expect(response.body.errors).toStrictEqual(errors); expect(response.body.errors).toEqual(errors);
}); });
}); });
@@ -193,7 +193,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-data', () => {
const notExistingStepUUID = Crypto.randomUUID(); const notExistingStepUUID = Crypto.randomUUID();
await request(app) await request(app)
.post(`/api/v1/steps/${notExistingStepUUID}/dynamic-data`) .get(`/api/v1/steps/${notExistingStepUUID}/dynamic-data`)
.set('Authorization', token) .set('Authorization', token)
.expect(404); .expect(404);
}); });
@@ -216,7 +216,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-data', () => {
const step = await createStep({ appKey: null }); const step = await createStep({ appKey: null });
await request(app) await request(app)
.post(`/api/v1/steps/${step.id}/dynamic-data`) .get(`/api/v1/steps/${step.id}/dynamic-data`)
.set('Authorization', token) .set('Authorization', token)
.expect(404); .expect(404);
}); });

View File

@@ -56,7 +56,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-fields', () => {
const expectedPayload = await createDynamicFieldsMock(); const expectedPayload = await createDynamicFieldsMock();
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
it('should return dynamically created fields of the another users step', async () => { it('should return dynamically created fields of the another users step', async () => {
@@ -97,7 +97,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-fields', () => {
const expectedPayload = await createDynamicFieldsMock(); const expectedPayload = await createDynamicFieldsMock();
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
it('should return not found response for not existing step UUID', async () => { it('should return not found response for not existing step UUID', async () => {
@@ -118,7 +118,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-fields', () => {
const notExistingStepUUID = Crypto.randomUUID(); const notExistingStepUUID = Crypto.randomUUID();
await request(app) await request(app)
.post(`/api/v1/steps/${notExistingStepUUID}/dynamic-fields`) .get(`/api/v1/steps/${notExistingStepUUID}/dynamic-fields`)
.set('Authorization', token) .set('Authorization', token)
.expect(404); .expect(404);
}); });
@@ -138,11 +138,10 @@ describe('POST /api/v1/steps/:stepId/dynamic-fields', () => {
conditions: [], conditions: [],
}); });
const step = await createStep(); const step = await createStep({ appKey: null });
await step.$query().patch({ appKey: null });
await request(app) await request(app)
.post(`/api/v1/steps/${step.id}/dynamic-fields`) .get(`/api/v1/steps/${step.id}/dynamic-fields`)
.set('Authorization', token) .set('Authorization', token)
.expect(404); .expect(404);
}); });

View File

@@ -43,7 +43,7 @@ describe('GET /api/v1/steps/:stepId/connection', () => {
const expectedPayload = await getConnectionMock(currentUserConnection); const expectedPayload = await getConnectionMock(currentUserConnection);
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
it('should return the current user connection data of specified step', async () => { it('should return the current user connection data of specified step', async () => {
@@ -70,7 +70,7 @@ describe('GET /api/v1/steps/:stepId/connection', () => {
const expectedPayload = await getConnectionMock(anotherUserConnection); const expectedPayload = await getConnectionMock(anotherUserConnection);
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
it('should return not found response for not existing step without connection', async () => { it('should return not found response for not existing step without connection', async () => {

View File

@@ -70,7 +70,7 @@ describe('GET /api/v1/steps/:stepId/previous-steps', () => {
[executionStepOne, executionStepTwo] [executionStepOne, executionStepTwo]
); );
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
it('should return the previous steps of the specified step of another user', async () => { it('should return the previous steps of the specified step of another user', async () => {
@@ -124,7 +124,7 @@ describe('GET /api/v1/steps/:stepId/previous-steps', () => {
[executionStepOne, executionStepTwo] [executionStepOne, executionStepTwo]
); );
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
it('should return not found response for not existing step UUID', async () => { it('should return not found response for not existing step UUID', async () => {

View File

@@ -79,7 +79,7 @@ describe('GET /api/v1/users/:userId/apps', () => {
.expect(200); .expect(200);
const expectedPayload = getAppsMock(); const expectedPayload = getAppsMock();
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
it('should return all apps of the another user', async () => { it('should return all apps of the another user', async () => {
@@ -143,7 +143,7 @@ describe('GET /api/v1/users/:userId/apps', () => {
.expect(200); .expect(200);
const expectedPayload = getAppsMock(); const expectedPayload = getAppsMock();
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
it('should return specified app of the current user', async () => { it('should return specified app of the current user', async () => {
@@ -204,7 +204,7 @@ describe('GET /api/v1/users/:userId/apps', () => {
.set('Authorization', token) .set('Authorization', token)
.expect(200); .expect(200);
expect(response.body.data.length).toStrictEqual(1); expect(response.body.data.length).toEqual(1);
expect(response.body.data[0].key).toStrictEqual('deepl'); expect(response.body.data[0].key).toEqual('deepl');
}); });
}); });

View File

@@ -39,6 +39,6 @@ describe('GET /api/v1/users/me', () => {
permissionTwo, permissionTwo,
]); ]);
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
}); });

View File

@@ -29,6 +29,6 @@ describe('GET /api/v1/user/invoices', () => {
const expectedPayload = await getInvoicesMock(invoices); const expectedPayload = await getInvoicesMock(invoices);
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
}); });

View File

@@ -36,7 +36,7 @@ describe('GET /api/v1/users/:userId/plan-and-usage', () => {
}, },
}; };
expect(response.body.data).toStrictEqual(expectedResponseData); expect(response.body.data).toEqual(expectedResponseData);
}); });
it('should return current plan and usage data', async () => { it('should return current plan and usage data', async () => {
@@ -63,6 +63,6 @@ describe('GET /api/v1/users/:userId/plan-and-usage', () => {
}, },
}; };
expect(response.body.data).toStrictEqual(expectedResponseData); expect(response.body.data).toEqual(expectedResponseData);
}); });
}); });

View File

@@ -33,7 +33,7 @@ describe('GET /api/v1/users/:userId/subscription', () => {
const expectedPayload = getSubscriptionMock(subscription); const expectedPayload = getSubscriptionMock(subscription);
expect(response.body).toStrictEqual(expectedPayload); expect(response.body).toEqual(expectedPayload);
}); });
it('should return not found response if there is no current subscription', async () => { it('should return not found response if there is no current subscription', async () => {

View File

@@ -32,7 +32,7 @@ describe('GET /api/v1/users/:userId/trial', () => {
.expect(200); .expect(200);
const expectedResponsePayload = await getUserTrialMock(user); const expectedResponsePayload = await getUserTrialMock(user);
expect(response.body).toStrictEqual(expectedResponsePayload); expect(response.body).toEqual(expectedResponsePayload);
}); });
}); });
}); });

View File

@@ -43,7 +43,7 @@ describe('PATCH /api/v1/users/:userId/password', () => {
.send(userData) .send(userData)
.expect(422); .expect(422);
expect(response.body.meta.type).toStrictEqual('ValidationError'); expect(response.body.meta.type).toEqual('ValidationError');
expect(response.body.errors).toMatchObject({ expect(response.body.errors).toMatchObject({
currentPassword: ['is incorrect.'], currentPassword: ['is incorrect.'],
}); });

View File

@@ -47,8 +47,7 @@ describe('PATCH /api/v1/users/:userId', () => {
.send(userData) .send(userData)
.expect(422); .expect(422);
expect(response.body.meta.type).toStrictEqual('ModelValidation'); expect(response.body.meta.type).toEqual('ModelValidation');
expect(response.body.errors).toMatchObject({ expect(response.body.errors).toMatchObject({
email: ['must be string'], email: ['must be string'],
fullName: ['must be string'], fullName: ['must be string'],

View File

@@ -1,52 +0,0 @@
export async function up(knex) {
await knex.schema.createTable('role_mappings', (table) => {
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
table
.uuid('saml_auth_provider_id')
.references('id')
.inTable('saml_auth_providers');
table.uuid('role_id').references('id').inTable('roles');
table.string('remote_role_name').notNullable();
table.unique(['saml_auth_provider_id', 'remote_role_name']);
table.timestamps(true, true);
});
const existingRoleMappings = await knex('saml_auth_providers_role_mappings');
if (existingRoleMappings.length) {
await knex('role_mappings').insert(existingRoleMappings);
}
return await knex.schema.dropTable('saml_auth_providers_role_mappings');
}
export async function down(knex) {
await knex.schema.createTable(
'saml_auth_providers_role_mappings',
(table) => {
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
table
.uuid('saml_auth_provider_id')
.references('id')
.inTable('saml_auth_providers');
table.uuid('role_id').references('id').inTable('roles');
table.string('remote_role_name').notNullable();
table.unique(['saml_auth_provider_id', 'remote_role_name']);
table.timestamps(true, true);
}
);
const existingRoleMappings = await knex('role_mappings');
if (existingRoleMappings.length) {
await knex('saml_auth_providers_role_mappings').insert(
existingRoleMappings
);
}
return await knex.schema.dropTable('role_mappings');
}

View File

@@ -1,11 +0,0 @@
export async function up(knex) {
return await knex.schema.alterTable('app_configs', (table) => {
table.boolean('use_only_predefined_auth_clients').defaultTo(false);
});
}
export async function down(knex) {
return await knex.schema.alterTable('app_configs', (table) => {
table.dropColumn('use_only_predefined_auth_clients');
});
}

View File

@@ -1,15 +0,0 @@
export async function up(knex) {
return await knex.schema.alterTable('app_configs', (table) => {
table.dropColumn('shared');
table.dropColumn('connection_allowed');
table.dropColumn('custom_connection_allowed');
});
}
export async function down(knex) {
return await knex.schema.alterTable('app_configs', (table) => {
table.boolean('shared').defaultTo(false);
table.boolean('connection_allowed').defaultTo(false);
table.boolean('custom_connection_allowed').defaultTo(false);
});
}

View File

@@ -30,7 +30,7 @@ const findOrCreateUserBySamlIdentity = async (
: [mappedUser.role]; : [mappedUser.role];
const samlAuthProviderRoleMapping = await samlAuthProvider const samlAuthProviderRoleMapping = await samlAuthProvider
.$relatedQuery('roleMappings') .$relatedQuery('samlAuthProvidersRoleMappings')
.whereIn('remote_role_name', mappedRoles) .whereIn('remote_role_name', mappedRoles)
.limit(1) .limit(1)
.first(); .first();

View File

@@ -1,46 +0,0 @@
import { describe, expect, it } from 'vitest';
import userAbility from './user-ability.js';
describe('userAbility', () => {
it('should return PureAbility instantiated with user permissions', () => {
const user = {
permissions: [
{
subject: 'Flow',
action: 'read',
conditions: ['isCreator'],
},
],
role: {
name: 'User',
},
};
const ability = userAbility(user);
expect(ability.rules).toStrictEqual(user.permissions);
});
it('should return permission-less PureAbility for user with no role', () => {
const user = {
permissions: [
{
subject: 'Flow',
action: 'read',
conditions: ['isCreator'],
},
],
role: null,
};
const ability = userAbility(user);
expect(ability.rules).toStrictEqual([]);
});
it('should return permission-less PureAbility for user with no permissions', () => {
const user = { permissions: null, role: { name: 'User' } };
const ability = userAbility(user);
expect(ability.rules).toStrictEqual([]);
});
});

View File

@@ -3,9 +3,17 @@
exports[`AppConfig model > jsonSchema should have correct validations 1`] = ` exports[`AppConfig model > jsonSchema should have correct validations 1`] = `
{ {
"properties": { "properties": {
"connectionAllowed": {
"default": false,
"type": "boolean",
},
"createdAt": { "createdAt": {
"type": "string", "type": "string",
}, },
"customConnectionAllowed": {
"default": false,
"type": "boolean",
},
"disabled": { "disabled": {
"default": false, "default": false,
"type": "boolean", "type": "boolean",
@@ -17,13 +25,13 @@ exports[`AppConfig model > jsonSchema should have correct validations 1`] = `
"key": { "key": {
"type": "string", "type": "string",
}, },
"updatedAt": { "shared": {
"type": "string",
},
"useOnlyPredefinedAuthClients": {
"default": false, "default": false,
"type": "boolean", "type": "boolean",
}, },
"updatedAt": {
"type": "string",
},
}, },
"required": [ "required": [
"key", "key",

View File

@@ -1,42 +0,0 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Flow model > jsonSchema should have correct validations 1`] = `
{
"properties": {
"active": {
"type": "boolean",
},
"createdAt": {
"type": "string",
},
"deletedAt": {
"type": "string",
},
"id": {
"format": "uuid",
"type": "string",
},
"name": {
"minLength": 1,
"type": "string",
},
"publishedAt": {
"type": "string",
},
"remoteWebhookId": {
"type": "string",
},
"updatedAt": {
"type": "string",
},
"userId": {
"format": "uuid",
"type": "string",
},
},
"required": [
"name",
],
"type": "object",
}
`;

View File

@@ -1,30 +0,0 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`RoleMapping model > jsonSchema should have the correct schema 1`] = `
{
"properties": {
"id": {
"format": "uuid",
"type": "string",
},
"remoteRoleName": {
"minLength": 1,
"type": "string",
},
"roleId": {
"format": "uuid",
"type": "string",
},
"samlAuthProviderId": {
"format": "uuid",
"type": "string",
},
},
"required": [
"samlAuthProviderId",
"roleId",
"remoteRoleName",
],
"type": "object",
}
`;

View File

@@ -1,33 +0,0 @@
// 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",
}
`;

View File

@@ -1,72 +0,0 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`SamlAuthProvider model > jsonSchema should have the correct schema 1`] = `
{
"properties": {
"active": {
"type": "boolean",
},
"certificate": {
"minLength": 1,
"type": "string",
},
"defaultRoleId": {
"format": "uuid",
"type": "string",
},
"emailAttributeName": {
"minLength": 1,
"type": "string",
},
"entryPoint": {
"minLength": 1,
"type": "string",
},
"firstnameAttributeName": {
"minLength": 1,
"type": "string",
},
"id": {
"format": "uuid",
"type": "string",
},
"issuer": {
"minLength": 1,
"type": "string",
},
"name": {
"minLength": 1,
"type": "string",
},
"roleAttributeName": {
"minLength": 1,
"type": "string",
},
"signatureAlgorithm": {
"enum": [
"sha1",
"sha256",
"sha512",
],
"type": "string",
},
"surnameAttributeName": {
"minLength": 1,
"type": "string",
},
},
"required": [
"name",
"certificate",
"signatureAlgorithm",
"entryPoint",
"issuer",
"firstnameAttributeName",
"surnameAttributeName",
"emailAttributeName",
"roleAttributeName",
"defaultRoleId",
],
"type": "object",
}
`;

View File

@@ -1,6 +1,6 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`RoleMapping model > jsonSchema should have the correct schema 1`] = ` exports[`SamlAuthProvidersRoleMapping model > jsonSchema should have the correct schema 1`] = `
{ {
"properties": { "properties": {
"id": { "id": {
@@ -28,3 +28,14 @@ exports[`RoleMapping model > jsonSchema should have the correct schema 1`] = `
"type": "object", "type": "object",
} }
`; `;
exports[`SamlAuthProvidersRoleMapping model > relationMappings should have samlAuthProvider relation 1`] = `
{
"join": {
"from": "saml_auth_providers_role_mappings.saml_auth_provider_id",
"to": "saml_auth_providers.id",
},
"modelClass": [Function],
"relation": [Function],
}
`;

View File

@@ -1,77 +0,0 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Step model > jsonSchema should have correct validations 1`] = `
{
"properties": {
"appKey": {
"maxLength": 255,
"minLength": 1,
"type": [
"string",
"null",
],
},
"connectionId": {
"format": "uuid",
"type": [
"string",
"null",
],
},
"createdAt": {
"type": "string",
},
"deletedAt": {
"type": "string",
},
"flowId": {
"format": "uuid",
"type": "string",
},
"id": {
"format": "uuid",
"type": "string",
},
"key": {
"type": [
"string",
"null",
],
},
"parameters": {
"type": "object",
},
"position": {
"type": "integer",
},
"status": {
"default": "incomplete",
"enum": [
"incomplete",
"completed",
],
"type": "string",
},
"type": {
"enum": [
"action",
"trigger",
],
"type": "string",
},
"updatedAt": {
"type": "string",
},
"webhookPath": {
"type": [
"string",
"null",
],
},
},
"required": [
"type",
],
"type": "object",
}
`;

View File

@@ -1,81 +0,0 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`User model > jsonSchema should have correct validations 1`] = `
{
"properties": {
"createdAt": {
"type": "string",
},
"deletedAt": {
"type": "string",
},
"email": {
"format": "email",
"maxLength": 255,
"minLength": 1,
"type": "string",
},
"fullName": {
"minLength": 1,
"type": "string",
},
"id": {
"format": "uuid",
"type": "string",
},
"invitationToken": {
"type": [
"string",
"null",
],
},
"invitationTokenSentAt": {
"format": "date-time",
"type": [
"string",
"null",
],
},
"password": {
"minLength": 6,
"type": "string",
},
"resetPasswordToken": {
"type": [
"string",
"null",
],
},
"resetPasswordTokenSentAt": {
"format": "date-time",
"type": [
"string",
"null",
],
},
"roleId": {
"format": "uuid",
"type": "string",
},
"status": {
"default": "active",
"enum": [
"active",
"invited",
],
"type": "string",
},
"trialExpiryDate": {
"type": "string",
},
"updatedAt": {
"type": "string",
},
},
"required": [
"fullName",
"email",
],
"type": "object",
}
`;

View File

@@ -60,26 +60,39 @@ class AppAuthClient extends Base {
return this.authDefaults ? true : false; return this.authDefaults ? true : false;
} }
async triggerAppConfigUpdate() {
const appConfig = await this.$relatedQuery('appConfig');
// This is a workaround to update connection allowed column for AppConfig
await appConfig?.$query().patch({
key: appConfig.key,
shared: appConfig.shared,
disabled: appConfig.disabled,
});
}
// TODO: Make another abstraction like beforeSave instead of using // TODO: Make another abstraction like beforeSave instead of using
// beforeInsert and beforeUpdate separately for the same operation. // beforeInsert and beforeUpdate separately for the same operation.
async $beforeInsert(queryContext) { async $beforeInsert(queryContext) {
await super.$beforeInsert(queryContext); await super.$beforeInsert(queryContext);
this.encryptData(); this.encryptData();
} }
async $afterInsert(queryContext) { async $afterInsert(queryContext) {
await super.$afterInsert(queryContext); await super.$afterInsert(queryContext);
await this.triggerAppConfigUpdate();
} }
async $beforeUpdate(opt, queryContext) { async $beforeUpdate(opt, queryContext) {
await super.$beforeUpdate(opt, queryContext); await super.$beforeUpdate(opt, queryContext);
this.encryptData(); this.encryptData();
} }
async $afterUpdate(opt, queryContext) { async $afterUpdate(opt, queryContext) {
await super.$afterUpdate(opt, queryContext); await super.$afterUpdate(opt, queryContext);
await this.triggerAppConfigUpdate();
} }
async $afterFind() { async $afterFind() {

View File

@@ -7,6 +7,7 @@ import AppAuthClient from './app-auth-client.js';
import Base from './base.js'; import Base from './base.js';
import appConfig from '../config/app.js'; import appConfig from '../config/app.js';
import { createAppAuthClient } from '../../test/factories/app-auth-client.js'; import { createAppAuthClient } from '../../test/factories/app-auth-client.js';
import { createAppConfig } from '../../test/factories/app-config.js';
describe('AppAuthClient model', () => { describe('AppAuthClient model', () => {
it('tableName should return correct name', () => { it('tableName should return correct name', () => {
@@ -68,9 +69,7 @@ describe('AppAuthClient model', () => {
); );
expect(formattedAuthDefaults).toStrictEqual(expectedDecryptedValue); expect(formattedAuthDefaults).toStrictEqual(expectedDecryptedValue);
expect(appAuthClient.authDefaults).not.toStrictEqual( expect(appAuthClient.authDefaults).not.toEqual(formattedAuthDefaults);
formattedAuthDefaults
);
}); });
it('should encrypt formattedAuthDefaults and remove formattedAuthDefaults', async () => { it('should encrypt formattedAuthDefaults and remove formattedAuthDefaults', async () => {
@@ -125,9 +124,7 @@ describe('AppAuthClient model', () => {
expect(appAuthClient.formattedAuthDefaults).toStrictEqual( expect(appAuthClient.formattedAuthDefaults).toStrictEqual(
formattedAuthDefaults formattedAuthDefaults
); );
expect(appAuthClient.authDefaults).not.toStrictEqual( expect(appAuthClient.authDefaults).not.toEqual(formattedAuthDefaults);
formattedAuthDefaults
);
}); });
}); });
@@ -163,6 +160,63 @@ describe('AppAuthClient model', () => {
}); });
}); });
describe('triggerAppConfigUpdate', () => {
it('should trigger an update in related app config', async () => {
await createAppConfig({ key: 'gitlab' });
const appAuthClient = await createAppAuthClient({
appKey: 'gitlab',
});
const appConfigBeforeUpdateSpy = vi.spyOn(
AppConfig.prototype,
'$beforeUpdate'
);
await appAuthClient.triggerAppConfigUpdate();
expect(appConfigBeforeUpdateSpy).toHaveBeenCalledOnce();
});
it('should update related AppConfig after creating an instance', async () => {
const appConfig = await createAppConfig({
key: 'gitlab',
disabled: false,
shared: true,
});
await createAppAuthClient({
appKey: 'gitlab',
active: true,
});
const refetchedAppConfig = await appConfig.$query();
expect(refetchedAppConfig.connectionAllowed).toBe(true);
});
it('should update related AppConfig after updating an instance', async () => {
const appConfig = await createAppConfig({
key: 'gitlab',
disabled: false,
shared: true,
});
const appAuthClient = await createAppAuthClient({
appKey: 'gitlab',
active: false,
});
let refetchedAppConfig = await appConfig.$query();
expect(refetchedAppConfig.connectionAllowed).toBe(false);
await appAuthClient.$query().patchAndFetch({ active: true });
refetchedAppConfig = await appConfig.$query();
expect(refetchedAppConfig.connectionAllowed).toBe(true);
});
});
it('$beforeInsert should call AppAuthClient.encryptData', async () => { it('$beforeInsert should call AppAuthClient.encryptData', async () => {
const appAuthClientBeforeInsertSpy = vi.spyOn( const appAuthClientBeforeInsertSpy = vi.spyOn(
AppAuthClient.prototype, AppAuthClient.prototype,
@@ -174,6 +228,17 @@ describe('AppAuthClient model', () => {
expect(appAuthClientBeforeInsertSpy).toHaveBeenCalledOnce(); expect(appAuthClientBeforeInsertSpy).toHaveBeenCalledOnce();
}); });
it('$afterInsert should call AppAuthClient.triggerAppConfigUpdate', async () => {
const appAuthClientAfterInsertSpy = vi.spyOn(
AppAuthClient.prototype,
'triggerAppConfigUpdate'
);
await createAppAuthClient();
expect(appAuthClientAfterInsertSpy).toHaveBeenCalledOnce();
});
it('$beforeUpdate should call AppAuthClient.encryptData', async () => { it('$beforeUpdate should call AppAuthClient.encryptData', async () => {
const appAuthClient = await createAppAuthClient(); const appAuthClient = await createAppAuthClient();
@@ -187,6 +252,19 @@ describe('AppAuthClient model', () => {
expect(appAuthClientBeforeUpdateSpy).toHaveBeenCalledOnce(); expect(appAuthClientBeforeUpdateSpy).toHaveBeenCalledOnce();
}); });
it('$afterUpdate should call AppAuthClient.triggerAppConfigUpdate', async () => {
const appAuthClient = await createAppAuthClient();
const appAuthClientAfterUpdateSpy = vi.spyOn(
AppAuthClient.prototype,
'triggerAppConfigUpdate'
);
await appAuthClient.$query().patchAndFetch({ name: 'sample' });
expect(appAuthClientAfterUpdateSpy).toHaveBeenCalledOnce();
});
it('$afterFind should call AppAuthClient.decryptData', async () => { it('$afterFind should call AppAuthClient.decryptData', async () => {
const appAuthClient = await createAppAuthClient(); const appAuthClient = await createAppAuthClient();

View File

@@ -16,7 +16,9 @@ class AppConfig extends Base {
properties: { properties: {
id: { type: 'string', format: 'uuid' }, id: { type: 'string', format: 'uuid' },
key: { type: 'string' }, key: { type: 'string' },
useOnlyPredefinedAuthClients: { type: 'boolean', default: false }, connectionAllowed: { type: 'boolean', default: false },
customConnectionAllowed: { type: 'boolean', default: false },
shared: { type: 'boolean', default: false },
disabled: { type: 'boolean', default: false }, disabled: { type: 'boolean', default: false },
createdAt: { type: 'string' }, createdAt: { type: 'string' },
updatedAt: { type: 'string' }, updatedAt: { type: 'string' },
@@ -39,6 +41,39 @@ class AppConfig extends Base {
return await App.findOneByKey(this.key); return await App.findOneByKey(this.key);
} }
async computeAndAssignConnectionAllowedProperty() {
this.connectionAllowed = await this.computeConnectionAllowedProperty();
}
async computeConnectionAllowedProperty() {
const appAuthClients = await this.$relatedQuery('appAuthClients');
const hasSomeActiveAppAuthClients =
appAuthClients?.some((appAuthClient) => appAuthClient.active) || false;
const conditions = [
hasSomeActiveAppAuthClients,
this.shared,
!this.disabled,
];
const connectionAllowed = conditions.every(Boolean);
return connectionAllowed;
}
async $beforeInsert(queryContext) {
await super.$beforeInsert(queryContext);
await this.computeAndAssignConnectionAllowedProperty();
}
async $beforeUpdate(opt, queryContext) {
await super.$beforeUpdate(opt, queryContext);
await this.computeAndAssignConnectionAllowedProperty();
}
} }
export default AppConfig; export default AppConfig;

View File

@@ -1,9 +1,11 @@
import { describe, it, expect } from 'vitest'; import { vi, describe, it, expect } from 'vitest';
import Base from './base.js'; import Base from './base.js';
import AppConfig from './app-config.js'; import AppConfig from './app-config.js';
import App from './app.js'; import App from './app.js';
import AppAuthClient from './app-auth-client.js'; import AppAuthClient from './app-auth-client.js';
import { createAppConfig } from '../../test/factories/app-config.js';
import { createAppAuthClient } from '../../test/factories/app-auth-client.js';
describe('AppConfig model', () => { describe('AppConfig model', () => {
it('tableName should return correct name', () => { it('tableName should return correct name', () => {
@@ -53,4 +55,126 @@ describe('AppConfig model', () => {
expect(app).toStrictEqual(expectedApp); expect(app).toStrictEqual(expectedApp);
}); });
}); });
describe('computeAndAssignConnectionAllowedProperty', () => {
it('should call computeConnectionAllowedProperty and assign the result', async () => {
const appConfig = await createAppConfig();
const computeConnectionAllowedPropertySpy = vi
.spyOn(appConfig, 'computeConnectionAllowedProperty')
.mockResolvedValue(true);
await appConfig.computeAndAssignConnectionAllowedProperty();
expect(computeConnectionAllowedPropertySpy).toHaveBeenCalled();
expect(appConfig.connectionAllowed).toBe(true);
});
});
describe('computeConnectionAllowedProperty', () => {
it('should return true when app is enabled, shared and allows custom connection with an active app auth client', async () => {
await createAppAuthClient({
appKey: 'deepl',
active: true,
});
await createAppAuthClient({
appKey: 'deepl',
active: false,
});
const appConfig = await createAppConfig({
disabled: false,
customConnectionAllowed: true,
shared: true,
key: 'deepl',
});
const connectionAllowed =
await appConfig.computeConnectionAllowedProperty();
expect(connectionAllowed).toBe(true);
});
it('should return false if there is no active app auth client', async () => {
await createAppAuthClient({
appKey: 'deepl',
active: false,
});
const appConfig = await createAppConfig({
disabled: false,
customConnectionAllowed: true,
shared: true,
key: 'deepl',
});
const connectionAllowed =
await appConfig.computeConnectionAllowedProperty();
expect(connectionAllowed).toBe(false);
});
it('should return false if there is no app auth clients', async () => {
const appConfig = await createAppConfig({
disabled: false,
customConnectionAllowed: true,
shared: true,
key: 'deepl',
});
const connectionAllowed =
await appConfig.computeConnectionAllowedProperty();
expect(connectionAllowed).toBe(false);
});
it('should return false when app is disabled', async () => {
const appConfig = await createAppConfig({
disabled: true,
customConnectionAllowed: true,
});
const connectionAllowed =
await appConfig.computeConnectionAllowedProperty();
expect(connectionAllowed).toBe(false);
});
it(`should return false when app doesn't allow custom connection`, async () => {
const appConfig = await createAppConfig({
disabled: false,
customConnectionAllowed: false,
});
const connectionAllowed =
await appConfig.computeConnectionAllowedProperty();
expect(connectionAllowed).toBe(false);
});
});
it('$beforeInsert should call computeAndAssignConnectionAllowedProperty', async () => {
const computeAndAssignConnectionAllowedPropertySpy = vi
.spyOn(AppConfig.prototype, 'computeAndAssignConnectionAllowedProperty')
.mockResolvedValue(true);
await createAppConfig();
expect(computeAndAssignConnectionAllowedPropertySpy).toHaveBeenCalledOnce();
});
it('$beforeUpdate should call computeAndAssignConnectionAllowedProperty', async () => {
const appConfig = await createAppConfig();
const computeAndAssignConnectionAllowedPropertySpy = vi
.spyOn(AppConfig.prototype, 'computeAndAssignConnectionAllowedProperty')
.mockResolvedValue(true);
await appConfig.$query().patch({
key: 'deepl',
});
expect(computeAndAssignConnectionAllowedPropertySpy).toHaveBeenCalledOnce();
});
}); });

View File

@@ -33,6 +33,10 @@ class Connection extends Base {
}, },
}; };
static get virtualAttributes() {
return ['reconnectable'];
}
static relationMappings = () => ({ static relationMappings = () => ({
user: { user: {
relation: Base.BelongsToOneRelation, relation: Base.BelongsToOneRelation,
@@ -79,6 +83,18 @@ class Connection extends Base {
}, },
}); });
get reconnectable() {
if (this.appAuthClientId) {
return this.appAuthClient.active;
}
if (this.appConfig) {
return !this.appConfig.disabled && this.appConfig.customConnectionAllowed;
}
return true;
}
encryptData() { encryptData() {
if (!this.eligibleForEncryption()) return; if (!this.eligibleForEncryption()) return;
@@ -128,13 +144,19 @@ class Connection extends Base {
); );
} }
if (appConfig.useOnlyPredefinedAuthClients && this.formattedData) { if (!appConfig.customConnectionAllowed && this.formattedData) {
throw new NotAuthorizedError( throw new NotAuthorizedError(
`New custom connections have been disabled for ${app.name}!` `New custom connections have been disabled for ${app.name}!`
); );
} }
if (!this.formattedData) { if (!appConfig.shared && this.appAuthClientId) {
throw new NotAuthorizedError(
'The connection with the given app auth client is not allowed!'
);
}
if (appConfig.shared && !this.formattedData) {
const authClient = await appConfig const authClient = await appConfig
.$relatedQuery('appAuthClients') .$relatedQuery('appAuthClients')
.findById(this.appAuthClientId) .findById(this.appAuthClientId)

View File

@@ -23,6 +23,14 @@ describe('Connection model', () => {
expect(Connection.jsonSchema).toMatchSnapshot(); expect(Connection.jsonSchema).toMatchSnapshot();
}); });
it('virtualAttributes should return correct attributes', () => {
const virtualAttributes = Connection.virtualAttributes;
const expectedAttributes = ['reconnectable'];
expect(virtualAttributes).toStrictEqual(expectedAttributes);
});
describe('relationMappings', () => { describe('relationMappings', () => {
it('should return correct associations', () => { it('should return correct associations', () => {
const relationMappings = Connection.relationMappings(); const relationMappings = Connection.relationMappings();
@@ -84,6 +92,78 @@ describe('Connection model', () => {
}); });
}); });
describe('reconnectable', () => {
it('should return active status of app auth client when created via app auth client', async () => {
const appAuthClient = await createAppAuthClient({
active: true,
formattedAuthDefaults: {
clientId: 'sample-id',
},
});
const connection = await createConnection({
appAuthClientId: appAuthClient.id,
formattedData: {
token: 'sample-token',
},
});
const connectionWithAppAuthClient = await connection
.$query()
.withGraphFetched({
appAuthClient: true,
});
expect(connectionWithAppAuthClient.reconnectable).toBe(true);
});
it('should return true when app config is not disabled and allows custom connection', async () => {
const appConfig = await createAppConfig({
key: 'gitlab',
disabled: false,
customConnectionAllowed: true,
});
const connection = await createConnection({
key: appConfig.key,
formattedData: {
token: 'sample-token',
},
});
const connectionWithAppAuthClient = await connection
.$query()
.withGraphFetched({
appConfig: true,
});
expect(connectionWithAppAuthClient.reconnectable).toBe(true);
});
it('should return false when app config is disabled or does not allow custom connection', async () => {
const connection = await createConnection({
key: 'gitlab',
formattedData: {
token: 'sample-token',
},
});
await createAppConfig({
key: 'gitlab',
disabled: true,
customConnectionAllowed: false,
});
const connectionWithAppAuthClient = await connection
.$query()
.withGraphFetched({
appConfig: true,
});
expect(connectionWithAppAuthClient.reconnectable).toBe(false);
});
});
describe('encryptData', () => { describe('encryptData', () => {
it('should return undefined if eligibleForEncryption is not true', async () => { it('should return undefined if eligibleForEncryption is not true', async () => {
vi.spyOn(Connection.prototype, 'eligibleForEncryption').mockReturnValue( vi.spyOn(Connection.prototype, 'eligibleForEncryption').mockReturnValue(
@@ -113,7 +193,7 @@ describe('Connection model', () => {
); );
expect(formattedData).toStrictEqual(expectedDecryptedValue); expect(formattedData).toStrictEqual(expectedDecryptedValue);
expect(connection.data).not.toStrictEqual(formattedData); expect(connection.data).not.toEqual(formattedData);
}); });
it('should encrypt formattedData and remove formattedData', async () => { it('should encrypt formattedData and remove formattedData', async () => {
@@ -163,7 +243,7 @@ describe('Connection model', () => {
connection.decryptData(); connection.decryptData();
expect(connection.formattedData).toStrictEqual(formattedData); expect(connection.formattedData).toStrictEqual(formattedData);
expect(connection.data).not.toStrictEqual(formattedData); expect(connection.data).not.toEqual(formattedData);
}); });
}); });
@@ -286,7 +366,6 @@ describe('Connection model', () => {
); );
}); });
// TODO: update test case name
it('should throw an error when app config does not allow custom connection with formatted data', async () => { it('should throw an error when app config does not allow custom connection with formatted data', async () => {
vi.spyOn(Connection.prototype, 'getApp').mockResolvedValue({ vi.spyOn(Connection.prototype, 'getApp').mockResolvedValue({
name: 'gitlab', name: 'gitlab',
@@ -294,7 +373,7 @@ describe('Connection model', () => {
vi.spyOn(Connection.prototype, 'getAppConfig').mockResolvedValue({ vi.spyOn(Connection.prototype, 'getAppConfig').mockResolvedValue({
disabled: false, disabled: false,
useOnlyPredefinedAuthClients: true, customConnectionAllowed: false,
}); });
const connection = new Connection(); const connection = new Connection();
@@ -307,10 +386,32 @@ describe('Connection model', () => {
); );
}); });
it('should throw an error when app config is not shared with app auth client', async () => {
vi.spyOn(Connection.prototype, 'getApp').mockResolvedValue({
name: 'gitlab',
});
vi.spyOn(Connection.prototype, 'getAppConfig').mockResolvedValue({
disabled: false,
shared: false,
});
const connection = new Connection();
connection.appAuthClientId = 'sample-id';
await expect(() =>
connection.checkEligibilityForCreation()
).rejects.toThrow(
'The connection with the given app auth client is not allowed!'
);
});
it('should apply app auth client auth defaults when creating with shared app auth client', async () => { it('should apply app auth client auth defaults when creating with shared app auth client', async () => {
await createAppConfig({ await createAppConfig({
key: 'gitlab', key: 'gitlab',
disabled: false, disabled: false,
customConnectionAllowed: true,
shared: true,
}); });
const appAuthClient = await createAppAuthClient({ const appAuthClient = await createAppAuthClient({

View File

@@ -88,13 +88,15 @@ class Flow extends Base {
}, },
}); });
static async populateStatusProperty(flows) { static async afterFind(args) {
const referenceFlow = flows[0]; const { result } = args;
const referenceFlow = result[0];
if (referenceFlow) { if (referenceFlow) {
const shouldBePaused = await referenceFlow.isPaused(); const shouldBePaused = await referenceFlow.isPaused();
for (const flow of flows) { for (const flow of result) {
if (!flow.active) { if (!flow.active) {
flow.status = 'draft'; flow.status = 'draft';
} else if (flow.active && shouldBePaused) { } else if (flow.active && shouldBePaused) {
@@ -106,10 +108,6 @@ class Flow extends Base {
} }
} }
static async afterFind(args) {
await this.populateStatusProperty(args.result);
}
async lastInternalId() { async lastInternalId() {
const lastExecution = await this.$relatedQuery('lastExecution'); const lastExecution = await this.$relatedQuery('lastExecution');
@@ -125,14 +123,13 @@ class Flow extends Base {
return lastExecutions.map((execution) => execution.internalId); return lastExecutions.map((execution) => execution.internalId);
} }
static get IncompleteStepsError() { get IncompleteStepsError() {
return new ValidationError({ return new ValidationError({
data: { data: {
flow: [ flow: [
{ {
message: message: 'All steps should be completed before updating flow status!'
'All steps should be completed before updating flow status!', }
},
], ],
}, },
type: 'incompleteStepsError', type: 'incompleteStepsError',
@@ -151,48 +148,36 @@ class Flow extends Base {
type: 'action', type: 'action',
position: 2, position: 2,
}); });
return this.$query().withGraphFetched('steps');
} }
async getStepById(stepId) { async createActionStep(previousStepId) {
return await this.$relatedQuery('steps').findById(stepId).throwIfNotFound(); const previousStep = await this.$relatedQuery('steps')
} .findById(previousStepId)
.throwIfNotFound();
async insertActionStepAtPosition(position) { const createdStep = await this.$relatedQuery('steps').insertAndFetch({
return await this.$relatedQuery('steps').insertAndFetch({
type: 'action', type: 'action',
position, position: previousStep.position + 1,
}); });
}
async getStepsAfterPosition(position) { const nextSteps = await this.$relatedQuery('steps')
return await this.$relatedQuery('steps').where('position', '>', position); .where('position', '>=', createdStep.position)
} .whereNot('id', createdStep.id);
async updateStepPositionsFrom(startPosition, steps) { const nextStepQueries = nextSteps.map(async (nextStep, index) => {
const stepPositionUpdates = steps.map(async (step, index) => { return await nextStep.$query().patchAndFetch({
return await step.$query().patch({ position: createdStep.position + index + 1,
position: startPosition + index,
}); });
}); });
return await Promise.all(stepPositionUpdates); await Promise.all(nextStepQueries);
}
async createStepAfter(previousStepId) {
const previousStep = await this.getStepById(previousStepId);
const nextSteps = await this.getStepsAfterPosition(previousStep.position);
const createdStep = await this.insertActionStepAtPosition(
previousStep.position + 1
);
await this.updateStepPositionsFrom(createdStep.position + 1, nextSteps);
return createdStep; return createdStep;
} }
async unregisterWebhook() { async delete() {
const triggerStep = await this.getTriggerStep(); const triggerStep = await this.getTriggerStep();
const trigger = await triggerStep?.getTriggerCommand(); const trigger = await triggerStep?.getTriggerCommand();
@@ -213,33 +198,15 @@ class Flow extends Base {
); );
} }
} }
}
async deleteExecutionSteps() {
const executionIds = ( const executionIds = (
await this.$relatedQuery('executions').select('executions.id') await this.$relatedQuery('executions').select('executions.id')
).map((execution) => execution.id); ).map((execution) => execution.id);
return await ExecutionStep.query() await ExecutionStep.query().delete().whereIn('execution_id', executionIds);
.delete()
.whereIn('execution_id', executionIds);
}
async deleteExecutions() {
return await this.$relatedQuery('executions').delete();
}
async deleteSteps() {
return await this.$relatedQuery('steps').delete();
}
async delete() {
await this.unregisterWebhook();
await this.deleteExecutionSteps();
await this.deleteExecutions();
await this.deleteSteps();
await this.$relatedQuery('executions').delete();
await this.$relatedQuery('steps').delete();
await this.$query().delete(); await this.$query().delete();
} }
@@ -324,18 +291,6 @@ class Flow extends Base {
return duplicatedFlowWithSteps; return duplicatedFlowWithSteps;
} }
async getTriggerStep() {
return await this.$relatedQuery('steps').findOne({
type: 'trigger',
});
}
async isPaused() {
const user = await this.$relatedQuery('user').withSoftDeleted();
const allowedToRunFlows = await user.isAllowedToRunFlows();
return allowedToRunFlows ? false : true;
}
async updateStatus(newActiveValue) { async updateStatus(newActiveValue) {
if (this.active === newActiveValue) { if (this.active === newActiveValue) {
return this; return this;
@@ -344,7 +299,7 @@ class Flow extends Base {
const triggerStep = await this.getTriggerStep(); const triggerStep = await this.getTriggerStep();
if (triggerStep.status === 'incomplete') { if (triggerStep.status === 'incomplete') {
throw Flow.IncompleteStepsError; throw this.IncompleteStepsError;
} }
const trigger = await triggerStep.getTriggerCommand(); const trigger = await triggerStep.getTriggerCommand();
@@ -398,55 +353,60 @@ class Flow extends Base {
}); });
} }
async throwIfHavingIncompleteSteps() { async $beforeUpdate(opt, queryContext) {
const incompleteStep = await this.$relatedQuery('steps').findOne({ await super.$beforeUpdate(opt, queryContext);
if (!this.active) return;
const oldFlow = opt.old;
const incompleteStep = await oldFlow.$relatedQuery('steps').findOne({
status: 'incomplete', status: 'incomplete',
}); });
if (incompleteStep) { if (incompleteStep) {
throw Flow.IncompleteStepsError; throw this.IncompleteStepsError;
} }
}
async throwIfHavingLessThanTwoSteps() { const allSteps = await oldFlow.$relatedQuery('steps');
const allSteps = await this.$relatedQuery('steps');
if (allSteps.length < 2) { if (allSteps.length < 2) {
throw new ValidationError({ throw new ValidationError({
data: { data: {
flow: [ flow: [
{ {
message: message: 'There should be at least one trigger and one action steps in the flow!'
'There should be at least one trigger and one action steps in the flow!', }
},
], ],
}, },
type: 'insufficientStepsError', type: 'insufficientStepsError',
}); });
} }
}
async $beforeUpdate(opt, queryContext) { return;
await super.$beforeUpdate(opt, queryContext);
if (this.active) {
await opt.old.throwIfHavingIncompleteSteps();
await opt.old.throwIfHavingLessThanTwoSteps();
}
} }
async $afterInsert(queryContext) { async $afterInsert(queryContext) {
await super.$afterInsert(queryContext); await super.$afterInsert(queryContext);
Telemetry.flowCreated(this); Telemetry.flowCreated(this);
} }
async $afterUpdate(opt, queryContext) { async $afterUpdate(opt, queryContext) {
await super.$afterUpdate(opt, queryContext); await super.$afterUpdate(opt, queryContext);
Telemetry.flowUpdated(this); Telemetry.flowUpdated(this);
} }
async getTriggerStep() {
return await this.$relatedQuery('steps').findOne({
type: 'trigger',
});
}
async isPaused() {
const user = await this.$relatedQuery('user').withSoftDeleted();
const allowedToRunFlows = await user.isAllowedToRunFlows();
return allowedToRunFlows ? false : true;
}
} }
export default Flow; export default Flow;

View File

@@ -1,616 +0,0 @@
import { describe, it, expect, vi } from 'vitest';
import Flow from './flow.js';
import User from './user.js';
import Base from './base.js';
import Step from './step.js';
import Execution from './execution.js';
import Telemetry from '../helpers/telemetry/index.js';
import * as globalVariableModule from '../helpers/global-variable.js';
import { createFlow } from '../../test/factories/flow.js';
import { createStep } from '../../test/factories/step.js';
import { createExecution } from '../../test/factories/execution.js';
import { createExecutionStep } from '../../test/factories/execution-step.js';
describe('Flow model', () => {
it('tableName should return correct name', () => {
expect(Flow.tableName).toBe('flows');
});
it('jsonSchema should have correct validations', () => {
expect(Flow.jsonSchema).toMatchSnapshot();
});
describe('relationMappings', () => {
it('should return correct associations', () => {
const relationMappings = Flow.relationMappings();
const expectedRelations = {
steps: {
relation: Base.HasManyRelation,
modelClass: Step,
join: {
from: 'flows.id',
to: 'steps.flow_id',
},
filter: expect.any(Function),
},
triggerStep: {
relation: Base.HasOneRelation,
modelClass: Step,
join: {
from: 'flows.id',
to: 'steps.flow_id',
},
filter: expect.any(Function),
},
executions: {
relation: Base.HasManyRelation,
modelClass: Execution,
join: {
from: 'flows.id',
to: 'executions.flow_id',
},
},
lastExecution: {
relation: Base.HasOneRelation,
modelClass: Execution,
join: {
from: 'flows.id',
to: 'executions.flow_id',
},
filter: expect.any(Function),
},
user: {
relation: Base.HasOneRelation,
modelClass: User,
join: {
from: 'flows.user_id',
to: 'users.id',
},
},
};
expect(relationMappings).toStrictEqual(expectedRelations);
});
it('steps should return the steps', () => {
const relations = Flow.relationMappings();
const orderBySpy = vi.fn();
relations.steps.filter({ orderBy: orderBySpy });
expect(orderBySpy).toHaveBeenCalledWith('position', 'asc');
});
it('triggerStep should return the trigger step', () => {
const relations = Flow.relationMappings();
const firstSpy = vi.fn();
const limitSpy = vi.fn().mockImplementation(() => ({
first: firstSpy,
}));
const whereSpy = vi.fn().mockImplementation(() => ({
limit: limitSpy,
}));
relations.triggerStep.filter({ where: whereSpy });
expect(whereSpy).toHaveBeenCalledWith('type', 'trigger');
expect(limitSpy).toHaveBeenCalledWith(1);
expect(firstSpy).toHaveBeenCalledOnce();
});
it('lastExecution should return the last execution', () => {
const relations = Flow.relationMappings();
const firstSpy = vi.fn();
const limitSpy = vi.fn().mockImplementation(() => ({
first: firstSpy,
}));
const orderBySpy = vi.fn().mockImplementation(() => ({
limit: limitSpy,
}));
relations.lastExecution.filter({ orderBy: orderBySpy });
expect(orderBySpy).toHaveBeenCalledWith('created_at', 'desc');
expect(limitSpy).toHaveBeenCalledWith(1);
expect(firstSpy).toHaveBeenCalledOnce();
});
});
describe('populateStatusProperty', () => {
it('should assign "draft" to status property when a flow is not active', async () => {
const referenceFlow = await createFlow({ active: false });
const flows = [referenceFlow];
vi.spyOn(referenceFlow, 'isPaused').mockResolvedValue();
await Flow.populateStatusProperty(flows);
expect(referenceFlow.status).toBe('draft');
});
it('should assign "paused" to status property when a flow is active, but should be paused', async () => {
const referenceFlow = await createFlow({ active: true });
const flows = [referenceFlow];
vi.spyOn(referenceFlow, 'isPaused').mockResolvedValue(true);
await Flow.populateStatusProperty(flows);
expect(referenceFlow.status).toBe('paused');
});
it('should assign "published" to status property when a flow is active', async () => {
const referenceFlow = await createFlow({ active: true });
const flows = [referenceFlow];
vi.spyOn(referenceFlow, 'isPaused').mockResolvedValue(false);
await Flow.populateStatusProperty(flows);
expect(referenceFlow.status).toBe('published');
});
});
it('afterFind should call Flow.populateStatusProperty', async () => {
const populateStatusPropertySpy = vi
.spyOn(Flow, 'populateStatusProperty')
.mockImplementation(() => {});
await createFlow();
expect(populateStatusPropertySpy).toHaveBeenCalledOnce();
});
describe('lastInternalId', () => {
it('should return internal ID of last execution when exists', async () => {
const flow = await createFlow();
await createExecution({ flowId: flow.id });
await createExecution({ flowId: flow.id });
const lastExecution = await createExecution({ flowId: flow.id });
expect(await flow.lastInternalId()).toBe(lastExecution.internalId);
});
it('should return null when no flow execution exists', async () => {
const flow = await createFlow();
expect(await flow.lastInternalId()).toBe(null);
});
});
describe('lastInternalIds', () => {
it('should return last internal IDs', async () => {
const flow = await createFlow();
const internalIds = [
await createExecution({ flowId: flow.id }),
await createExecution({ flowId: flow.id }),
await createExecution({ flowId: flow.id }),
].map((execution) => execution.internalId);
expect(await flow.lastInternalIds()).toStrictEqual(internalIds);
});
it('should return last 50 internal IDs by default', async () => {
const flow = new Flow();
const limitSpy = vi.fn().mockResolvedValue([]);
vi.spyOn(flow, '$relatedQuery').mockReturnValue({
select: vi.fn().mockReturnThis(),
orderBy: vi.fn().mockReturnThis(),
limit: limitSpy,
});
await flow.lastInternalIds();
expect(limitSpy).toHaveBeenCalledWith(50);
});
});
it('IncompleteStepsError should return validation error for incomplete steps', () => {
expect(() => {
throw Flow.IncompleteStepsError;
}).toThrowError(
'flow: All steps should be completed before updating flow status!'
);
});
it('createInitialSteps should create one trigger and one action step', async () => {
const flow = await createFlow();
await flow.createInitialSteps();
const steps = await flow.$relatedQuery('steps');
expect(steps.length).toBe(2);
expect(steps[0]).toMatchObject({
flowId: flow.id,
type: 'trigger',
position: 1,
});
expect(steps[1]).toMatchObject({
flowId: flow.id,
type: 'action',
position: 2,
});
});
it('getStepById should return the step with the given ID from the flow', async () => {
const flow = await createFlow();
const step = await createStep({ flowId: flow.id });
expect(await flow.getStepById(step.id)).toStrictEqual(step);
});
it('insertActionStepAtPosition should insert action step at given position', async () => {
const flow = await createFlow();
await flow.createInitialSteps();
const createdStep = await flow.insertActionStepAtPosition(2);
expect(createdStep).toMatchObject({
type: 'action',
position: 2,
});
});
it('getStepsAfterPosition should return steps after the given position', async () => {
const flow = await createFlow();
await flow.createInitialSteps();
await createStep({ flowId: flow.id });
expect(await flow.getStepsAfterPosition(1)).toMatchObject([
{ position: 2 },
{ position: 3 },
]);
});
it('updateStepPositionsFrom', async () => {
const flow = await createFlow();
await createStep({ type: 'trigger', flowId: flow.id, position: 6 });
await createStep({ type: 'action', flowId: flow.id, position: 8 });
await createStep({ type: 'action', flowId: flow.id, position: 10 });
await flow.updateStepPositionsFrom(2, await flow.$relatedQuery('steps'));
expect(await flow.$relatedQuery('steps')).toMatchObject([
{ position: 2, type: 'trigger' },
{ position: 3, type: 'action' },
{ position: 4, type: 'action' },
]);
});
it('createStepAfter should create an action step after given step ID', async () => {
const flow = await createFlow();
const triggerStep = await createStep({ type: 'trigger', flowId: flow.id });
const actionStep = await createStep({ type: 'action', flowId: flow.id });
const createdStep = await flow.createStepAfter(triggerStep.id);
const refetchedActionStep = await actionStep.$query();
expect(createdStep).toMatchObject({ type: 'action', position: 2 });
expect(refetchedActionStep.position).toBe(3);
});
describe('unregisterWebhook', () => {
it('should unregister webhook on remote when supported', async () => {
const flow = await createFlow();
const triggerStep = await createStep({
flowId: flow.id,
appKey: 'typeform',
key: 'new-entry',
type: 'trigger',
});
const unregisterHookSpy = vi.fn().mockResolvedValue();
vi.spyOn(Step.prototype, 'getTriggerCommand').mockResolvedValue({
type: 'webhook',
unregisterHook: unregisterHookSpy,
});
const globalVariableSpy = vi
.spyOn(globalVariableModule, 'default')
.mockResolvedValue('global-variable');
await flow.unregisterWebhook();
expect(unregisterHookSpy).toHaveBeenCalledWith('global-variable');
expect(globalVariableSpy).toHaveBeenCalledWith({
flow,
step: triggerStep,
connection: undefined,
app: await triggerStep.getApp(),
});
});
it('should silently fail when unregistration fails', async () => {
const flow = await createFlow();
await createStep({
flowId: flow.id,
appKey: 'typeform',
key: 'new-entry',
type: 'trigger',
});
const unregisterHookSpy = vi.fn().mockRejectedValue(new Error());
vi.spyOn(Step.prototype, 'getTriggerCommand').mockResolvedValue({
type: 'webhook',
unregisterHook: unregisterHookSpy,
});
expect(await flow.unregisterWebhook()).toBe(undefined);
expect(unregisterHookSpy).toHaveBeenCalledOnce();
});
it('should do nothing when trigger step is not webhook', async () => {
const flow = await createFlow();
await createStep({
flowId: flow.id,
type: 'trigger',
});
const unregisterHookSpy = vi.fn().mockRejectedValue(new Error());
expect(await flow.unregisterWebhook()).toBe(undefined);
expect(unregisterHookSpy).not.toHaveBeenCalled();
});
});
it('deleteExecutionSteps should delete related execution steps', async () => {
const flow = await createFlow();
const execution = await createExecution({ flowId: flow.id });
const firstExecutionStep = await createExecutionStep({
executionId: execution.id,
});
const secondExecutionStep = await createExecutionStep({
executionId: execution.id,
});
await flow.deleteExecutionSteps();
expect(await firstExecutionStep.$query()).toBe(undefined);
expect(await secondExecutionStep.$query()).toBe(undefined);
});
it('deleteExecutions should delete related executions', async () => {
const flow = await createFlow();
const firstExecution = await createExecution({ flowId: flow.id });
const secondExecution = await createExecution({ flowId: flow.id });
await flow.deleteExecutions();
expect(await firstExecution.$query()).toBe(undefined);
expect(await secondExecution.$query()).toBe(undefined);
});
it('deleteSteps should delete related steps', async () => {
const flow = await createFlow();
await flow.createInitialSteps();
await flow.deleteSteps();
expect(await flow.$relatedQuery('steps')).toStrictEqual([]);
});
it('delete should delete the flow with its relations', async () => {
const flow = await createFlow();
const unregisterWebhookSpy = vi
.spyOn(flow, 'unregisterWebhook')
.mockResolvedValue();
const deleteExecutionStepsSpy = vi
.spyOn(flow, 'deleteExecutionSteps')
.mockResolvedValue();
const deleteExecutionsSpy = vi
.spyOn(flow, 'deleteExecutions')
.mockResolvedValue();
const deleteStepsSpy = vi.spyOn(flow, 'deleteSteps').mockResolvedValue();
await flow.delete();
expect(unregisterWebhookSpy).toHaveBeenCalledOnce();
expect(deleteExecutionStepsSpy).toHaveBeenCalledOnce();
expect(deleteExecutionsSpy).toHaveBeenCalledOnce();
expect(deleteStepsSpy).toHaveBeenCalledOnce();
expect(await flow.$query()).toBe(undefined);
});
it.todo('duplicateFor');
it('getTriggerStep', async () => {
const flow = await createFlow();
const triggerStep = await createStep({ flowId: flow.id, type: 'trigger' });
await createStep({ flowId: flow.id, type: 'action' });
expect(await flow.getTriggerStep()).toStrictEqual(triggerStep);
});
describe('isPaused', () => {
it('should return true when user.isAllowedToRunFlows returns false', async () => {
const flow = await createFlow();
const isAllowedToRunFlowsSpy = vi.fn().mockResolvedValue(false);
vi.spyOn(flow, '$relatedQuery').mockReturnValue({
withSoftDeleted: vi.fn().mockReturnThis(),
isAllowedToRunFlows: isAllowedToRunFlowsSpy,
});
expect(await flow.isPaused()).toBe(true);
expect(isAllowedToRunFlowsSpy).toHaveBeenCalledOnce();
});
it('should return false when user.isAllowedToRunFlows returns true', async () => {
const flow = await createFlow();
const isAllowedToRunFlowsSpy = vi.fn().mockResolvedValue(true);
vi.spyOn(flow, '$relatedQuery').mockReturnValue({
withSoftDeleted: vi.fn().mockReturnThis(),
isAllowedToRunFlows: isAllowedToRunFlowsSpy,
});
expect(await flow.isPaused()).toBe(false);
expect(isAllowedToRunFlowsSpy).toHaveBeenCalledOnce();
});
});
describe('throwIfHavingIncompleteSteps', () => {
it('should throw validation error with incomplete steps', async () => {
const flow = await createFlow();
await flow.createInitialSteps();
await expect(() =>
flow.throwIfHavingIncompleteSteps()
).rejects.toThrowError(
'flow: All steps should be completed before updating flow status!'
);
});
it('should return undefined when all steps are completed', async () => {
const flow = await createFlow();
await createStep({
flowId: flow.id,
status: 'completed',
type: 'trigger',
});
await createStep({
flowId: flow.id,
status: 'completed',
type: 'action',
});
expect(await flow.throwIfHavingIncompleteSteps()).toBe(undefined);
});
});
describe('throwIfHavingLessThanTwoSteps', () => {
it('should throw validation error with less than two steps', async () => {
const flow = await createFlow();
await expect(() =>
flow.throwIfHavingLessThanTwoSteps()
).rejects.toThrowError(
'flow: There should be at least one trigger and one action steps in the flow!'
);
});
it('should return undefined when there are at least two steps', async () => {
const flow = await createFlow();
await createStep({
flowId: flow.id,
type: 'trigger',
});
await createStep({
flowId: flow.id,
type: 'action',
});
expect(await flow.throwIfHavingLessThanTwoSteps()).toBe(undefined);
});
});
describe('$beforeUpdate', () => {
it('should invoke throwIfHavingIncompleteSteps when flow is becoming active', async () => {
const flow = await createFlow({ active: false });
const throwIfHavingIncompleteStepsSpy = vi
.spyOn(Flow.prototype, 'throwIfHavingIncompleteSteps')
.mockImplementation(() => {});
const throwIfHavingLessThanTwoStepsSpy = vi
.spyOn(Flow.prototype, 'throwIfHavingLessThanTwoSteps')
.mockImplementation(() => {});
await flow.$query().patch({ active: true });
expect(throwIfHavingIncompleteStepsSpy).toHaveBeenCalledOnce();
expect(throwIfHavingLessThanTwoStepsSpy).toHaveBeenCalledOnce();
});
it('should invoke throwIfHavingIncompleteSteps when flow is not becoming active', async () => {
const flow = await createFlow({ active: true });
const throwIfHavingIncompleteStepsSpy = vi
.spyOn(Flow.prototype, 'throwIfHavingIncompleteSteps')
.mockImplementation(() => {});
const throwIfHavingLessThanTwoStepsSpy = vi
.spyOn(Flow.prototype, 'throwIfHavingLessThanTwoSteps')
.mockImplementation(() => {});
await flow.$query().patch({});
expect(throwIfHavingIncompleteStepsSpy).not.toHaveBeenCalledOnce();
expect(throwIfHavingLessThanTwoStepsSpy).not.toHaveBeenCalledOnce();
});
});
describe('$afterInsert', () => {
it('should call super.$afterInsert', async () => {
const superAfterInsertSpy = vi.spyOn(Base.prototype, '$afterInsert');
await createFlow();
expect(superAfterInsertSpy).toHaveBeenCalled();
});
it('should call Telemetry.flowCreated', async () => {
const telemetryFlowCreatedSpy = vi
.spyOn(Telemetry, 'flowCreated')
.mockImplementation(() => {});
const flow = await createFlow();
expect(telemetryFlowCreatedSpy).toHaveBeenCalledWith(flow);
});
});
describe('$afterUpdate', () => {
it('should call super.$afterUpdate', async () => {
const superAfterUpdateSpy = vi.spyOn(Base.prototype, '$afterUpdate');
const flow = await createFlow();
await flow.$query().patch({ active: false });
expect(superAfterUpdateSpy).toHaveBeenCalledOnce();
});
it('$afterUpdate should call Telemetry.flowUpdated', async () => {
const telemetryFlowUpdatedSpy = vi
.spyOn(Telemetry, 'flowUpdated')
.mockImplementation(() => {});
const flow = await createFlow();
await flow.$query().patch({ active: false });
expect(telemetryFlowUpdatedSpy).toHaveBeenCalled({});
});
});
});

View File

@@ -52,64 +52,57 @@ class Role extends Base {
return await this.query().findOne({ name: 'Admin' }); return await this.query().findOne({ name: 'Admin' });
} }
async preventAlteringAdmin() { async updateWithPermissions(data) {
const currentRole = await Role.query().findById(this.id); if (this.isAdmin) {
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;
await this.updatePermissions(permissions); return await Role.transaction(async (trx) => {
await this.$relatedQuery('permissions', trx).delete();
await this.$query().patchAndFetch({ if (permissions?.length) {
id: this.id, const validPermissions = Permission.filter(permissions).map(
name, (permission) => ({
description, ...permission,
}); roleId: this.id,
})
);
return await this.$query() await Permission.query().insert(validPermissions);
.leftJoinRelated({ }
permissions: true,
}) await this.$query(trx).patch({
.withGraphFetched({ name,
permissions: true, description,
}); });
return await this.$query(trx)
.leftJoinRelated({
permissions: true,
})
.withGraphFetched({
permissions: true,
});
});
} }
async deleteWithPermissions() { async deleteWithPermissions() {
await this.deletePermissions(); return await Role.transaction(async (trx) => {
await this.$relatedQuery('permissions', trx).delete();
return await this.$query().delete(); return await this.$query(trx).delete();
});
} }
async assertNoRoleUserExists() { async $beforeDelete(queryContext) {
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;
@@ -125,9 +118,7 @@ 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,
@@ -149,26 +140,6 @@ 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;

Some files were not shown because too many files have changed in this diff Show More