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
196 changed files with 18579 additions and 23552 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,13 +3,12 @@ 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/**' - 'packages/e2e-tests/**'
# - 'packages/e2e-tests/**' - 'packages/web/**'
# - 'packages/web/**' - '!packages/backend/src/apps/**'
# - '!packages/backend/src/apps/**'
workflow_dispatch: workflow_dispatch:
env: env:
@@ -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

@@ -59,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

@@ -83,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

@@ -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

@@ -32,7 +32,7 @@ describe('GET /api/v1/apps/:appKey/config', () => {
.expect(200); .expect(200);
const expectedPayload = getAppConfigMock(appConfig); const expectedPayload = getAppConfigMock(appConfig);
expect(response.body).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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -69,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 () => {
@@ -126,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
);
}); });
}); });

View File

@@ -193,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 () => {
@@ -243,7 +243,7 @@ describe('Connection model', () => {
connection.decryptData(); connection.decryptData();
expect(connection.formattedData).toStrictEqual(formattedData); expect(connection.formattedData).toStrictEqual(formattedData);
expect(connection.data).not.toStrictEqual(formattedData); expect(connection.data).not.toEqual(formattedData);
}); });
}); });

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;

View File

@@ -1,287 +0,0 @@
import { describe, it, expect, vi } from 'vitest';
import Role from './role';
import Base from './base.js';
import Permission from './permission.js';
import User from './user.js';
import { createRole } from '../../test/factories/role.js';
import { createPermission } from '../../test/factories/permission.js';
import { createUser } from '../../test/factories/user.js';
import { createSamlAuthProvider } from '../../test/factories/saml-auth-provider.ee.js';
describe('Role model', () => {
it('tableName should return correct name', () => {
expect(Role.tableName).toBe('roles');
});
it('jsonSchema should have correct validations', () => {
expect(Role.jsonSchema).toMatchSnapshot();
});
it('relationMappingsshould return correct associations', () => {
const relationMappings = Role.relationMappings();
const expectedRelations = {
users: {
relation: Base.HasManyRelation,
modelClass: User,
join: {
from: 'roles.id',
to: 'users.role_id',
},
},
permissions: {
relation: Base.HasManyRelation,
modelClass: Permission,
join: {
from: 'roles.id',
to: 'permissions.role_id',
},
},
};
expect(relationMappings).toStrictEqual(expectedRelations);
});
it('virtualAttributes should return correct attributes', () => {
expect(Role.virtualAttributes).toStrictEqual(['isAdmin']);
});
describe('isAdmin', () => {
it('should return true for admin named role', () => {
const role = new Role();
role.name = 'Admin';
expect(role.isAdmin).toBe(true);
});
it('should return false for not admin named roles', () => {
const role = new Role();
role.name = 'User';
expect(role.isAdmin).toBe(false);
});
});
it('findAdmin should return admin role', async () => {
const createdAdminRole = await createRole({ name: 'Admin' });
const adminRole = await Role.findAdmin();
expect(createdAdminRole).toStrictEqual(adminRole);
});
describe('preventAlteringAdmin', () => {
it('preventAlteringAdmin should throw an error when altering admin role', async () => {
const role = await createRole({ name: 'Admin' });
await expect(() => role.preventAlteringAdmin()).rejects.toThrowError(
'The admin role cannot be altered!'
);
});
it('preventAlteringAdmin should not throw an error when altering non-admin roles', async () => {
const role = await createRole({ name: 'User' });
expect(await role.preventAlteringAdmin()).toBe(undefined);
});
});
it("deletePermissions should delete role's permissions", async () => {
const role = await createRole({ name: 'User' });
await createPermission({ roleId: role.id });
await role.deletePermissions();
expect(await role.$relatedQuery('permissions')).toStrictEqual([]);
});
describe('createPermissions', () => {
it('should create permissions', async () => {
const role = await createRole({ name: 'User' });
await role.createPermissions([
{ action: 'read', subject: 'Flow', conditions: [] },
]);
expect(await role.$relatedQuery('permissions')).toMatchObject([
{
action: 'read',
subject: 'Flow',
conditions: [],
},
]);
});
it('should call Permission.filter', async () => {
const role = await createRole({ name: 'User' });
const permissions = [{ action: 'read', subject: 'Flow', conditions: [] }];
const permissionFilterSpy = vi
.spyOn(Permission, 'filter')
.mockReturnValue(permissions);
await role.createPermissions(permissions);
expect(permissionFilterSpy).toHaveBeenCalledWith(permissions);
});
});
it('updatePermissions should delete existing permissions and create new permissions', async () => {
const permissionsData = [
{ action: 'read', subject: 'Flow', conditions: [] },
];
const deletePermissionsSpy = vi
.spyOn(Role.prototype, 'deletePermissions')
.mockResolvedValueOnce();
const createPermissionsSpy = vi
.spyOn(Role.prototype, 'createPermissions')
.mockResolvedValueOnce();
const role = await createRole({ name: 'User' });
await role.updatePermissions(permissionsData);
expect(deletePermissionsSpy.mock.invocationCallOrder[0]).toBeLessThan(
createPermissionsSpy.mock.invocationCallOrder[0]
);
expect(deletePermissionsSpy).toHaveBeenNthCalledWith(1);
expect(createPermissionsSpy).toHaveBeenNthCalledWith(1, permissionsData);
});
describe('updateWithPermissions', () => {
it('should update role along with given permissions', async () => {
const role = await createRole({ name: 'User' });
await createPermission({
roleId: role.id,
subject: 'Flow',
action: 'read',
conditions: [],
});
const newRoleData = {
name: 'Updated user',
description: 'Updated description',
permissions: [
{
action: 'update',
subject: 'Flow',
conditions: [],
},
],
};
await role.updateWithPermissions(newRoleData);
const roleWithPermissions = await role
.$query()
.leftJoinRelated({ permissions: true })
.withGraphFetched({ permissions: true });
expect(roleWithPermissions).toMatchObject(newRoleData);
});
});
describe('deleteWithPermissions', () => {
it('should delete role along with given permissions', async () => {
const role = await createRole({ name: 'User' });
await createPermission({
roleId: role.id,
subject: 'Flow',
action: 'read',
conditions: [],
});
await role.deleteWithPermissions();
const refetchedRole = await role.$query();
const rolePermissions = await Permission.query().where({
roleId: role.id,
});
expect(refetchedRole).toBe(undefined);
expect(rolePermissions).toStrictEqual([]);
});
});
describe('assertNoRoleUserExists', () => {
it('should reject with an error when the role has users', async () => {
const role = await createRole({ name: 'User' });
await createUser({ roleId: role.id });
await expect(() => role.assertNoRoleUserExists()).rejects.toThrowError(
`All users must be migrated away from the "User" role.`
);
});
it('should resolve when the role does not have any users', async () => {
const role = await createRole();
expect(await role.assertNoRoleUserExists()).toBe(undefined);
});
});
describe('assertNoConfigurationUsage', () => {
it('should reject with an error when the role is used in configuration', async () => {
const role = await createRole();
await createSamlAuthProvider({ defaultRoleId: role.id });
await expect(() =>
role.assertNoConfigurationUsage()
).rejects.toThrowError(
'samlAuthProvider: You need to change the default role in the SAML configuration before deleting this role.'
);
});
it('should resolve when the role does not have any users', async () => {
const role = await createRole();
expect(await role.assertNoConfigurationUsage()).toBe(undefined);
});
});
it('assertRoleIsNotUsed should call assertNoRoleUserExists and assertNoConfigurationUsage', async () => {
const role = new Role();
const assertNoRoleUserExistsSpy = vi
.spyOn(role, 'assertNoRoleUserExists')
.mockResolvedValue();
const assertNoConfigurationUsageSpy = vi
.spyOn(role, 'assertNoConfigurationUsage')
.mockResolvedValue();
await role.assertRoleIsNotUsed();
expect(assertNoRoleUserExistsSpy).toHaveBeenCalledOnce();
expect(assertNoConfigurationUsageSpy).toHaveBeenCalledOnce();
});
describe('$beforeDelete', () => {
it('should call preventAlteringAdmin', async () => {
const role = await createRole({ name: 'User' });
const preventAlteringAdminSpy = vi
.spyOn(role, 'preventAlteringAdmin')
.mockResolvedValue();
await role.$query().delete();
expect(preventAlteringAdminSpy).toHaveBeenCalledOnce();
});
it('should call assertRoleIsNotUsed', async () => {
const role = await createRole({ name: 'User' });
const assertRoleIsNotUsedSpy = vi
.spyOn(role, 'assertRoleIsNotUsed')
.mockResolvedValue();
await role.$query().delete();
expect(assertRoleIsNotUsedSpy).toHaveBeenCalledOnce();
});
});
});

View File

@@ -5,7 +5,7 @@ import appConfig from '../config/app.js';
import axios from '../helpers/axios-with-proxy.js'; import axios from '../helpers/axios-with-proxy.js';
import Base from './base.js'; import Base from './base.js';
import Identity from './identity.ee.js'; import Identity from './identity.ee.js';
import RoleMapping from './role-mapping.ee.js'; import SamlAuthProvidersRoleMapping from './saml-auth-providers-role-mapping.ee.js';
class SamlAuthProvider extends Base { class SamlAuthProvider extends Base {
static tableName = 'saml_auth_providers'; static tableName = 'saml_auth_providers';
@@ -53,12 +53,12 @@ class SamlAuthProvider extends Base {
to: 'saml_auth_providers.id', to: 'saml_auth_providers.id',
}, },
}, },
roleMappings: { samlAuthProvidersRoleMappings: {
relation: Base.HasManyRelation, relation: Base.HasManyRelation,
modelClass: RoleMapping, modelClass: SamlAuthProvidersRoleMapping,
join: { join: {
from: 'saml_auth_providers.id', from: 'saml_auth_providers.id',
to: 'role_mappings.saml_auth_provider_id', to: 'saml_auth_providers_role_mappings.saml_auth_provider_id',
}, },
}, },
}); });
@@ -133,22 +133,27 @@ class SamlAuthProvider extends Base {
} }
async updateRoleMappings(roleMappings) { async updateRoleMappings(roleMappings) {
await this.$relatedQuery('roleMappings').delete(); return await SamlAuthProvider.transaction(async (trx) => {
await this.$relatedQuery('samlAuthProvidersRoleMappings', trx).delete();
if (isEmpty(roleMappings)) { if (isEmpty(roleMappings)) {
return []; return [];
} }
const roleMappingsData = roleMappings.map((roleMapping) => ({ const samlAuthProvidersRoleMappingsData = roleMappings.map(
...roleMapping, (samlAuthProvidersRoleMapping) => ({
samlAuthProviderId: this.id, ...samlAuthProvidersRoleMapping,
})); samlAuthProviderId: this.id,
})
);
const newRoleMappings = await RoleMapping.query().insertAndFetch( const samlAuthProvidersRoleMappings =
roleMappingsData await SamlAuthProvidersRoleMapping.query(trx).insertAndFetch(
); samlAuthProvidersRoleMappingsData
);
return newRoleMappings; return samlAuthProvidersRoleMappings;
});
} }
} }

View File

@@ -1,231 +0,0 @@
import { vi, beforeEach, describe, it, expect } from 'vitest';
import { v4 as uuidv4 } from 'uuid';
import SamlAuthProvider from '../models/saml-auth-provider.ee';
import RoleMapping from '../models/role-mapping.ee';
import axios from '../helpers/axios-with-proxy.js';
import Identity from './identity.ee';
import Base from './base';
import appConfig from '../config/app';
import { createSamlAuthProvider } from '../../test/factories/saml-auth-provider.ee.js';
import { createRoleMapping } from '../../test/factories/role-mapping.js';
import { createRole } from '../../test/factories/role.js';
describe('SamlAuthProvider model', () => {
it('tableName should return correct name', () => {
expect(SamlAuthProvider.tableName).toBe('saml_auth_providers');
});
it('jsonSchema should have the correct schema', () => {
expect(SamlAuthProvider.jsonSchema).toMatchSnapshot();
});
it('relationMappings should return correct associations', () => {
const relationMappings = SamlAuthProvider.relationMappings();
const expectedRelations = {
identities: {
relation: Base.HasOneRelation,
modelClass: Identity,
join: {
from: 'identities.provider_id',
to: 'saml_auth_providers.id',
},
},
roleMappings: {
relation: Base.HasManyRelation,
modelClass: RoleMapping,
join: {
from: 'saml_auth_providers.id',
to: 'role_mappings.saml_auth_provider_id',
},
},
};
expect(relationMappings).toStrictEqual(expectedRelations);
});
it('virtualAttributes should return correct attributes', () => {
const virtualAttributes = SamlAuthProvider.virtualAttributes;
const expectedAttributes = ['loginUrl', 'remoteLogoutUrl'];
expect(virtualAttributes).toStrictEqual(expectedAttributes);
});
it('loginUrl should return the URL of login', () => {
const samlAuthProvider = new SamlAuthProvider();
samlAuthProvider.issuer = 'sample-issuer';
vi.spyOn(appConfig, 'baseUrl', 'get').mockReturnValue(
'https://automatisch.io'
);
expect(samlAuthProvider.loginUrl).toStrictEqual(
'https://automatisch.io/login/saml/sample-issuer'
);
});
it('loginCallbackUrl should return the URL of login callback', () => {
const samlAuthProvider = new SamlAuthProvider();
samlAuthProvider.issuer = 'sample-issuer';
vi.spyOn(appConfig, 'baseUrl', 'get').mockReturnValue(
'https://automatisch.io'
);
expect(samlAuthProvider.loginCallBackUrl).toStrictEqual(
'https://automatisch.io/login/saml/sample-issuer/callback'
);
});
it('remoteLogoutUrl should return the URL from entrypoint', () => {
const samlAuthProvider = new SamlAuthProvider();
samlAuthProvider.entryPoint = 'https://example.com/saml/logout';
expect(samlAuthProvider.remoteLogoutUrl).toStrictEqual(
'https://example.com/saml/logout'
);
});
it('config should return the correct configuration object', () => {
const samlAuthProvider = new SamlAuthProvider();
samlAuthProvider.certificate = 'sample-certificate';
samlAuthProvider.signatureAlgorithm = 'sha256';
samlAuthProvider.entryPoint = 'https://example.com/saml';
samlAuthProvider.issuer = 'sample-issuer';
vi.spyOn(appConfig, 'baseUrl', 'get').mockReturnValue(
'https://automatisch.io'
);
const expectedConfig = {
callbackUrl: 'https://automatisch.io/login/saml/sample-issuer/callback',
cert: 'sample-certificate',
entryPoint: 'https://example.com/saml',
issuer: 'sample-issuer',
signatureAlgorithm: 'sha256',
logoutUrl: 'https://example.com/saml',
};
expect(samlAuthProvider.config).toStrictEqual(expectedConfig);
});
it('generateLogoutRequestBody should return a correctly encoded SAML logout request', () => {
vi.mock('uuid', () => ({
v4: vi.fn(),
}));
const samlAuthProvider = new SamlAuthProvider();
samlAuthProvider.entryPoint = 'https://example.com/saml';
samlAuthProvider.issuer = 'sample-issuer';
const mockUuid = '123e4567-e89b-12d3-a456-426614174000';
uuidv4.mockReturnValue(mockUuid);
const sessionId = 'test-session-id';
const logoutRequest = samlAuthProvider.generateLogoutRequestBody(sessionId);
const expectedLogoutRequest = `
<samlp:LogoutRequest
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
ID="${mockUuid}"
Version="2.0"
IssueInstant="${new Date().toISOString()}"
Destination="https://example.com/saml">
<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">sample-issuer</saml:Issuer>
<samlp:SessionIndex>test-session-id</samlp:SessionIndex>
</samlp:LogoutRequest>
`;
const expectedEncodedRequest = Buffer.from(expectedLogoutRequest).toString(
'base64'
);
expect(logoutRequest).toBe(expectedEncodedRequest);
});
it('terminateRemoteSession should send the correct POST request and return the response', async () => {
vi.mock('../helpers/axios-with-proxy.js', () => ({
default: {
post: vi.fn(),
},
}));
const samlAuthProvider = new SamlAuthProvider();
samlAuthProvider.entryPoint = 'https://example.com/saml';
samlAuthProvider.generateLogoutRequestBody = vi
.fn()
.mockReturnValue('mockEncodedLogoutRequest');
const sessionId = 'test-session-id';
const mockResponse = { data: 'Logout Successful' };
axios.post.mockResolvedValue(mockResponse);
const response = await samlAuthProvider.terminateRemoteSession(sessionId);
expect(samlAuthProvider.generateLogoutRequestBody).toHaveBeenCalledWith(
sessionId
);
expect(axios.post).toHaveBeenCalledWith(
'https://example.com/saml',
'SAMLRequest=mockEncodedLogoutRequest',
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
}
);
expect(response).toBe(mockResponse);
});
describe('updateRoleMappings', () => {
let samlAuthProvider;
beforeEach(async () => {
samlAuthProvider = await createSamlAuthProvider();
});
it('should remove all existing role mappings', async () => {
await createRoleMapping({
samlAuthProviderId: samlAuthProvider.id,
remoteRoleName: 'Admin',
});
await createRoleMapping({
samlAuthProviderId: samlAuthProvider.id,
remoteRoleName: 'User',
});
await samlAuthProvider.updateRoleMappings([]);
const roleMappings = await samlAuthProvider.$relatedQuery('roleMappings');
expect(roleMappings).toStrictEqual([]);
});
it('should return the updated role mappings when new ones are provided', async () => {
const adminRole = await createRole({ name: 'Admin' });
const userRole = await createRole({ name: 'User' });
const newRoleMappings = [
{ remoteRoleName: 'Admin', roleId: adminRole.id },
{ remoteRoleName: 'User', roleId: userRole.id },
];
const result = await samlAuthProvider.updateRoleMappings(newRoleMappings);
const refetchedRoleMappings = await samlAuthProvider.$relatedQuery(
'roleMappings'
);
expect(result).toStrictEqual(refetchedRoleMappings);
});
});
});

View File

@@ -1,8 +1,8 @@
import Base from './base.js'; import Base from './base.js';
import SamlAuthProvider from './saml-auth-provider.ee.js'; import SamlAuthProvider from './saml-auth-provider.ee.js';
class RoleMapping extends Base { class SamlAuthProvidersRoleMapping extends Base {
static tableName = 'role_mappings'; static tableName = 'saml_auth_providers_role_mappings';
static jsonSchema = { static jsonSchema = {
type: 'object', type: 'object',
@@ -21,11 +21,11 @@ class RoleMapping extends Base {
relation: Base.BelongsToOneRelation, relation: Base.BelongsToOneRelation,
modelClass: SamlAuthProvider, modelClass: SamlAuthProvider,
join: { join: {
from: 'role_mappings.saml_auth_provider_id', from: 'saml_auth_providers_role_mappings.saml_auth_provider_id',
to: 'saml_auth_providers.id', to: 'saml_auth_providers.id',
}, },
}, },
}); });
} }
export default RoleMapping; export default SamlAuthProvidersRoleMapping;

View File

@@ -1,26 +1,28 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import RoleMapping from './role-mapping.ee'; import SamlAuthProvidersRoleMapping from '../models/saml-auth-providers-role-mapping.ee';
import SamlAuthProvider from './saml-auth-provider.ee'; import SamlAuthProvider from './saml-auth-provider.ee';
import Base from './base'; import Base from './base';
describe('RoleMapping model', () => { describe('SamlAuthProvidersRoleMapping model', () => {
it('tableName should return correct name', () => { it('tableName should return correct name', () => {
expect(RoleMapping.tableName).toBe('role_mappings'); expect(SamlAuthProvidersRoleMapping.tableName).toBe(
'saml_auth_providers_role_mappings'
);
}); });
it('jsonSchema should have the correct schema', () => { it('jsonSchema should have the correct schema', () => {
expect(RoleMapping.jsonSchema).toMatchSnapshot(); expect(SamlAuthProvidersRoleMapping.jsonSchema).toMatchSnapshot();
}); });
it('relationMappings should return correct associations', () => { it('relationMappings should return correct associations', () => {
const relationMappings = RoleMapping.relationMappings(); const relationMappings = SamlAuthProvidersRoleMapping.relationMappings();
const expectedRelations = { const expectedRelations = {
samlAuthProvider: { samlAuthProvider: {
relation: Base.BelongsToOneRelation, relation: Base.BelongsToOneRelation,
modelClass: SamlAuthProvider, modelClass: SamlAuthProvider,
join: { join: {
from: 'role_mappings.saml_auth_provider_id', from: 'saml_auth_providers_role_mappings.saml_auth_provider_id',
to: 'saml_auth_providers.id', to: 'saml_auth_providers.id',
}, },
}, },

View File

@@ -93,14 +93,6 @@ class Step extends Base {
return `${appConfig.baseUrl}/apps/${this.appKey}/assets/favicon.svg`; return `${appConfig.baseUrl}/apps/${this.appKey}/assets/favicon.svg`;
} }
get isTrigger() {
return this.type === 'trigger';
}
get isAction() {
return this.type === 'action';
}
async computeWebhookPath() { async computeWebhookPath() {
if (this.type === 'action') return null; if (this.type === 'action') return null;
@@ -143,6 +135,24 @@ class Step extends Base {
return webhookUrl; return webhookUrl;
} }
async $afterInsert(queryContext) {
await super.$afterInsert(queryContext);
Telemetry.stepCreated(this);
}
async $afterUpdate(opt, queryContext) {
await super.$afterUpdate(opt, queryContext);
Telemetry.stepUpdated(this);
}
get isTrigger() {
return this.type === 'trigger';
}
get isAction() {
return this.type === 'action';
}
async getApp() { async getApp() {
if (!this.appKey) return null; if (!this.appKey) return null;
@@ -160,7 +170,12 @@ class Step extends Base {
} }
async getLastExecutionStep() { async getLastExecutionStep() {
return await this.$relatedQuery('lastExecutionStep'); const lastExecutionStep = await this.$relatedQuery('executionSteps')
.orderBy('created_at', 'desc')
.limit(1)
.first();
return lastExecutionStep;
} }
async getNextStep() { async getNextStep() {
@@ -192,18 +207,19 @@ class Step extends Base {
} }
async getSetupFields() { async getSetupFields() {
let substeps; let setupSupsteps;
if (this.isTrigger) { if (this.isTrigger) {
substeps = (await this.getTriggerCommand()).substeps; setupSupsteps = (await this.getTriggerCommand()).substeps;
} else { } else {
substeps = (await this.getActionCommand()).substeps; setupSupsteps = (await this.getActionCommand()).substeps;
} }
const setupSubstep = substeps.find( const existingArguments = setupSupsteps.find(
(substep) => substep.key === 'chooseTrigger' (substep) => substep.key === 'chooseTrigger'
); ).arguments;
return setupSubstep.arguments;
return existingArguments;
} }
async getSetupAndDynamicFields() { async getSetupAndDynamicFields() {
@@ -310,17 +326,23 @@ class Step extends Base {
.$relatedQuery('steps') .$relatedQuery('steps')
.where('position', '>', this.position); .where('position', '>', this.position);
await flow.updateStepPositionsFrom(this.position, nextSteps); const nextStepQueries = nextSteps.map(async (nextStep) => {
await nextStep.$query().patch({
position: nextStep.position - 1,
});
});
await Promise.all(nextStepQueries);
} }
async updateFor(user, newStepData) { async updateFor(user, newStepData) {
const { appKey = this.appKey, connectionId, key, parameters } = newStepData; const { connectionId, appKey, key, parameters } = newStepData;
if (connectionId && appKey) { if (connectionId && (appKey || this.appKey)) {
await user.authorizedConnections await user.authorizedConnections
.findOne({ .findOne({
id: connectionId, id: connectionId,
key: appKey, key: appKey || this.appKey,
}) })
.throwIfNotFound(); .throwIfNotFound();
} }
@@ -334,8 +356,8 @@ class Step extends Base {
} }
const updatedStep = await this.$query().patchAndFetch({ const updatedStep = await this.$query().patchAndFetch({
key, key: key,
appKey, appKey: appKey,
connectionId: connectionId, connectionId: connectionId,
parameters: parameters, parameters: parameters,
status: 'incomplete', status: 'incomplete',
@@ -345,16 +367,6 @@ class Step extends Base {
return updatedStep; return updatedStep;
} }
async $afterInsert(queryContext) {
await super.$afterInsert(queryContext);
Telemetry.stepCreated(this);
}
async $afterUpdate(opt, queryContext) {
await super.$afterUpdate(opt, queryContext);
Telemetry.stepUpdated(this);
}
} }
export default Step; export default Step;

View File

@@ -1,504 +0,0 @@
import { beforeEach, describe, it, expect, vi } from 'vitest';
import appConfig from '../config/app.js';
import App from './app.js';
import Base from './base.js';
import Step from './step.js';
import Flow from './flow.js';
import Connection from './connection.js';
import ExecutionStep from './execution-step.js';
import Telemetry from '../helpers/telemetry/index.js';
import * as testRunModule from '../services/test-run.js';
import { createFlow } from '../../test/factories/flow.js';
import { createUser } from '../../test/factories/user.js';
import { createRole } from '../../test/factories/role.js';
import { createPermission } from '../../test/factories/permission.js';
import { createConnection } from '../../test/factories/connection.js';
import { createStep } from '../../test/factories/step.js';
import { createExecutionStep } from '../../test/factories/execution-step.js';
describe('Step model', () => {
it('tableName should return correct name', () => {
expect(Step.tableName).toBe('steps');
});
it('jsonSchema should have correct validations', () => {
expect(Step.jsonSchema).toMatchSnapshot();
});
it('virtualAttributes should return correct attributes', () => {
const virtualAttributes = Step.virtualAttributes;
const expectedAttributes = ['iconUrl', 'webhookUrl'];
expect(virtualAttributes).toStrictEqual(expectedAttributes);
});
describe('relationMappings', () => {
it('should return correct associations', () => {
const relationMappings = Step.relationMappings();
const expectedRelations = {
flow: {
relation: Base.BelongsToOneRelation,
modelClass: Flow,
join: {
from: 'steps.flow_id',
to: 'flows.id',
},
},
connection: {
relation: Base.HasOneRelation,
modelClass: Connection,
join: {
from: 'steps.connection_id',
to: 'connections.id',
},
},
lastExecutionStep: {
relation: Base.HasOneRelation,
modelClass: ExecutionStep,
join: {
from: 'steps.id',
to: 'execution_steps.step_id',
},
filter: expect.any(Function),
},
executionSteps: {
relation: Base.HasManyRelation,
modelClass: ExecutionStep,
join: {
from: 'steps.id',
to: 'execution_steps.step_id',
},
},
};
expect(relationMappings).toStrictEqual(expectedRelations);
});
it('lastExecutionStep should return the trigger step', () => {
const relations = Step.relationMappings();
const firstSpy = vi.fn();
const limitSpy = vi.fn().mockImplementation(() => ({
first: firstSpy,
}));
const orderBySpy = vi.fn().mockImplementation(() => ({
limit: limitSpy,
}));
relations.lastExecutionStep.filter({ orderBy: orderBySpy });
expect(orderBySpy).toHaveBeenCalledWith('created_at', 'desc');
expect(limitSpy).toHaveBeenCalledWith(1);
expect(firstSpy).toHaveBeenCalledOnce();
});
});
describe('webhookUrl', () => {
it('should return it along with appConfig.webhookUrl when exists', () => {
vi.spyOn(appConfig, 'webhookUrl', 'get').mockReturnValue(
'https://automatisch.io'
);
const step = new Step();
step.webhookPath = '/webhook-path';
expect(step.webhookUrl).toBe('https://automatisch.io/webhook-path');
});
it('should return null when webhookUrl does not exist', () => {
const step = new Step();
expect(step.webhookUrl).toBe(null);
});
});
describe('iconUrl', () => {
it('should return step app icon absolute URL when app is set', () => {
vi.spyOn(appConfig, 'baseUrl', 'get').mockReturnValue(
'https://automatisch.io'
);
const step = new Step();
step.appKey = 'gitlab';
expect(step.iconUrl).toBe(
'https://automatisch.io/apps/gitlab/assets/favicon.svg'
);
});
it('should return null when appKey is not set', () => {
const step = new Step();
expect(step.iconUrl).toBe(null);
});
});
it('isTrigger should return true when step type is trigger', () => {
const step = new Step();
step.type = 'trigger';
expect(step.isTrigger).toBe(true);
});
it('isAction should return true when step type is action', () => {
const step = new Step();
step.type = 'action';
expect(step.isAction).toBe(true);
});
describe.todo('computeWebhookPath');
describe('getWebhookUrl', () => {
it('should return absolute webhook URL when step type is trigger', async () => {
const step = new Step();
step.type = 'trigger';
vi.spyOn(step, 'computeWebhookPath').mockResolvedValue('/webhook-path');
vi.spyOn(appConfig, 'webhookUrl', 'get').mockReturnValue(
'https://automatisch.io'
);
expect(await step.getWebhookUrl()).toBe(
'https://automatisch.io/webhook-path'
);
});
it('should return undefined when step type is action', async () => {
const step = new Step();
step.type = 'action';
expect(await step.getWebhookUrl()).toBe(undefined);
});
});
describe('getApp', () => {
it('should return app with the given appKey', async () => {
const step = new Step();
step.appKey = 'gitlab';
const findOneByKeySpy = vi.spyOn(App, 'findOneByKey').mockResolvedValue();
await step.getApp();
expect(findOneByKeySpy).toHaveBeenCalledWith('gitlab');
});
it('should return null with no appKey', async () => {
const step = new Step();
const findOneByKeySpy = vi.spyOn(App, 'findOneByKey').mockResolvedValue();
expect(await step.getApp()).toBe(null);
expect(findOneByKeySpy).not.toHaveBeenCalled();
});
});
it('test should execute the flow and mark the step as completed', async () => {
const step = await createStep({ status: 'incomplete' });
const testRunSpy = vi.spyOn(testRunModule, 'default').mockResolvedValue();
const updatedStep = await step.test();
expect(testRunSpy).toHaveBeenCalledWith({ stepId: step.id });
expect(updatedStep.status).toBe('completed');
});
it('getLastExecutionStep should return last execution step', async () => {
const step = await createStep();
await createExecutionStep({ stepId: step.id });
const secondExecutionStep = await createExecutionStep({ stepId: step.id });
expect(await step.getLastExecutionStep()).toStrictEqual(
secondExecutionStep
);
});
it('getNextStep should return the next step', async () => {
const firstStep = await createStep();
const secondStep = await createStep({ flowId: firstStep.flowId });
const thirdStep = await createStep({ flowId: firstStep.flowId });
expect(await secondStep.getNextStep()).toStrictEqual(thirdStep);
});
describe('getTriggerCommand', () => {
it('should return trigger command when app key and key are defined in trigger step', async () => {
const step = new Step();
step.type = 'trigger';
step.appKey = 'webhook';
step.key = 'catchRawWebhook';
const findOneByKeySpy = vi.spyOn(App, 'findOneByKey');
const triggerCommand = await step.getTriggerCommand();
expect(findOneByKeySpy).toHaveBeenCalledWith(step.appKey);
expect(triggerCommand.key).toBe(step.key);
});
it('should return null when key is not defined', async () => {
const step = new Step();
step.type = 'trigger';
step.appKey = 'webhook';
expect(await step.getTriggerCommand()).toBe(null);
});
});
describe('getActionCommand', () => {
it('should return action comamand when app key and key are defined in action step', async () => {
const step = new Step();
step.type = 'action';
step.appKey = 'ntfy';
step.key = 'sendMessage';
const findOneByKeySpy = vi.spyOn(App, 'findOneByKey');
const actionCommand = await step.getActionCommand();
expect(findOneByKeySpy).toHaveBeenCalledWith(step.appKey);
expect(actionCommand.key).toBe(step.key);
});
it('should return null when key is not defined', async () => {
const step = new Step();
step.type = 'action';
step.appKey = 'ntfy';
expect(await step.getActionCommand()).toBe(null);
});
});
describe('getSetupFields', () => {
it('should return trigger setup substep fields in trigger step', async () => {
const step = new Step();
step.appKey = 'webhook';
step.key = 'catchRawWebhook';
step.type = 'trigger';
expect(await step.getSetupFields()).toStrictEqual([
{
label: 'Wait until flow is done',
key: 'workSynchronously',
type: 'dropdown',
required: true,
options: [
{ label: 'Yes', value: true },
{ label: 'No', value: false },
],
},
]);
});
it('should return action setup substep fields in action step', async () => {
const step = new Step();
step.appKey = 'datastore';
step.key = 'getValue';
step.type = 'action';
expect(await step.getSetupFields()).toStrictEqual([
{
label: 'Key',
key: 'key',
type: 'string',
required: true,
description: 'The key of your value to get.',
variables: true,
},
]);
});
});
it.todo('getSetupAndDynamicFields');
it.todo('createDynamicFields');
it.todo('createDynamicData');
it.todo('updateWebhookUrl');
describe('delete', () => {
it('should delete the step and align the positions', async () => {
const flow = await createFlow();
await createStep({ flowId: flow.id, position: 1, type: 'trigger' });
await createStep({ flowId: flow.id, position: 2 });
const stepToDelete = await createStep({ flowId: flow.id, position: 3 });
await createStep({ flowId: flow.id, position: 4 });
await stepToDelete.delete();
const steps = await flow.$relatedQuery('steps');
const stepIds = steps.map((step) => step.id);
expect(stepIds).not.toContain(stepToDelete.id);
});
it('should align the positions of remaining steps', async () => {
const flow = await createFlow();
await createStep({ flowId: flow.id, position: 1, type: 'trigger' });
await createStep({ flowId: flow.id, position: 2 });
const stepToDelete = await createStep({ flowId: flow.id, position: 3 });
await createStep({ flowId: flow.id, position: 4 });
await stepToDelete.delete();
const steps = await flow.$relatedQuery('steps');
const stepPositions = steps.map((step) => step.position);
expect(stepPositions).toMatchObject([1, 2, 3]);
});
it('should delete related execution steps', async () => {
const step = await createStep();
const executionStep = await createExecutionStep({ stepId: step.id });
await step.delete();
expect(await executionStep.$query()).toBe(undefined);
});
});
describe('updateFor', async () => {
let step,
userRole,
user,
userConnection,
anotherUser,
anotherUserConnection;
beforeEach(async () => {
userRole = await createRole({ name: 'User' });
anotherUser = await createUser({ roleId: userRole.id });
user = await createUser({ roleId: userRole.id });
userConnection = await createConnection({
key: 'deepl',
userId: user.id,
});
anotherUserConnection = await createConnection({
key: 'deepl',
userId: anotherUser.id,
});
await createPermission({
roleId: userRole.id,
action: 'read',
subject: 'Connection',
conditions: ['isCreator'],
});
step = await createStep();
});
it('should update step with the given payload and mark it as incomplete', async () => {
const stepData = {
appKey: 'deepl',
key: 'translateText',
connectionId: anotherUserConnection.id,
parameters: {
key: 'value',
},
};
const anotherUserWithRoleAndPermissions = await anotherUser
.$query()
.withGraphFetched({ permissions: true, role: true });
const updatedStep = await step.updateFor(
anotherUserWithRoleAndPermissions,
stepData
);
expect(updatedStep).toMatchObject({
...stepData,
status: 'incomplete',
});
});
it('should invoke updateWebhookUrl', async () => {
const updateWebhookUrlSpy = vi
.spyOn(Step.prototype, 'updateWebhookUrl')
.mockResolvedValue();
const stepData = {
appKey: 'deepl',
key: 'translateText',
};
await step.updateFor(user, stepData);
expect(updateWebhookUrlSpy).toHaveBeenCalledOnce();
});
it('should not update step when inaccessible connection is given', async () => {
const stepData = {
appKey: 'deepl',
key: 'translateText',
connectionId: userConnection.id,
};
const anotherUserWithRoleAndPermissions = await anotherUser
.$query()
.withGraphFetched({ permissions: true, role: true });
await expect(() =>
step.updateFor(anotherUserWithRoleAndPermissions, stepData)
).rejects.toThrowError('NotFoundError');
});
it('should not update step when given app key and key do not exist', async () => {
const stepData = {
appKey: 'deepl',
key: 'not-existing-key',
};
await expect(() => step.updateFor(user, stepData)).rejects.toThrowError(
'DeepL does not have an action with the "not-existing-key" key!'
);
});
});
describe('$afterInsert', () => {
it('should call super.$afterInsert', async () => {
const superAfterInsertSpy = vi.spyOn(Base.prototype, '$afterInsert');
await createStep();
expect(superAfterInsertSpy).toHaveBeenCalled();
});
it('should call Telemetry.stepCreated', async () => {
const telemetryStepCreatedSpy = vi
.spyOn(Telemetry, 'stepCreated')
.mockImplementation(() => {});
const step = await createStep();
expect(telemetryStepCreatedSpy).toHaveBeenCalledWith(step);
});
});
describe('$afterUpdate', () => {
it('should call super.$afterUpdate', async () => {
const superAfterUpdateSpy = vi.spyOn(Base.prototype, '$afterUpdate');
const step = await createStep();
await step.$query().patch({ position: 2 });
expect(superAfterUpdateSpy).toHaveBeenCalledOnce();
});
it('$afterUpdate should call Telemetry.stepUpdated', async () => {
const telemetryStepUpdatedSpy = vi
.spyOn(Telemetry, 'stepUpdated')
.mockImplementation(() => {});
const step = await createStep();
await step.$query().patch({ position: 2 });
expect(telemetryStepUpdatedSpy).toHaveBeenCalled({});
});
});
});

View File

@@ -212,10 +212,6 @@ class User extends Base {
return `${appConfig.webAppUrl}/accept-invitation?token=${this.invitationToken}`; return `${appConfig.webAppUrl}/accept-invitation?token=${this.invitationToken}`;
} }
get ability() {
return userAbility(this);
}
static async authenticate(email, password) { static async authenticate(email, password) {
const user = await User.query().findOne({ const user = await User.query().findOne({
email: email?.toLowerCase() || null, email: email?.toLowerCase() || null,
@@ -227,8 +223,8 @@ class User extends Base {
} }
} }
async login(password) { login(password) {
return await bcrypt.compare(password, this.password); return bcrypt.compare(password, this.password);
} }
async generateResetPasswordToken() { async generateResetPasswordToken() {
@@ -411,7 +407,7 @@ class User extends Base {
} }
} }
startTrialPeriod() { async startTrialPeriod() {
this.trialExpiryDate = DateTime.now().plus({ days: 30 }).toISODate(); this.trialExpiryDate = DateTime.now().plus({ days: 30 }).toISODate();
} }
@@ -587,30 +583,32 @@ class User extends Base {
return user; return user;
} }
can(action, subject) { async $beforeInsert(queryContext) {
const can = this.ability.can(action, subject); await super.$beforeInsert(queryContext);
if (!can) throw new NotAuthorizedError('The user is not authorized!'); this.email = this.email.toLowerCase();
await this.generateHash();
const relevantRule = this.ability.relevantRuleFor(action, subject); if (appConfig.isCloud) {
await this.startTrialPeriod();
const conditions = relevantRule?.conditions || [];
const conditionMap = Object.fromEntries(
conditions.map((condition) => [condition, true])
);
return conditionMap;
}
lowercaseEmail() {
if (this.email) {
this.email = this.email.toLowerCase();
} }
} }
async createUsageData() { async $beforeUpdate(opt, queryContext) {
await super.$beforeUpdate(opt, queryContext);
if (this.email) {
this.email = this.email.toLowerCase();
}
await this.generateHash();
}
async $afterInsert(queryContext) {
await super.$afterInsert(queryContext);
if (appConfig.isCloud) { if (appConfig.isCloud) {
return await this.$relatedQuery('usageData').insertAndFetch({ await this.$relatedQuery('usageData').insert({
userId: this.id, userId: this.id,
consumedTaskCount: 0, consumedTaskCount: 0,
nextResetAt: DateTime.now().plus({ days: 30 }).toISODate(), nextResetAt: DateTime.now().plus({ days: 30 }).toISODate(),
@@ -618,10 +616,8 @@ class User extends Base {
} }
} }
async omitEnterprisePermissionsWithoutValidLicense() { async $afterFind() {
if (await hasValidLicense()) { if (await hasValidLicense()) return this;
return this;
}
if (Array.isArray(this.permissions)) { if (Array.isArray(this.permissions)) {
this.permissions = this.permissions.filter((permission) => { this.permissions = this.permissions.filter((permission) => {
@@ -635,35 +631,35 @@ class User extends Base {
return !restrictedSubjects.includes(permission.subject); return !restrictedSubjects.includes(permission.subject);
}); });
} }
return this;
} }
async $beforeInsert(queryContext) { get ability() {
await super.$beforeInsert(queryContext); return userAbility(this);
this.lowercaseEmail();
await this.generateHash();
if (appConfig.isCloud) {
this.startTrialPeriod();
}
} }
async $beforeUpdate(opt, queryContext) { can(action, subject) {
await super.$beforeUpdate(opt, queryContext); const can = this.ability.can(action, subject);
this.lowercaseEmail(); if (!can) throw new NotAuthorizedError();
await this.generateHash(); const relevantRule = this.ability.relevantRuleFor(action, subject);
const conditions = relevantRule?.conditions || [];
const conditionMap = Object.fromEntries(
conditions.map((condition) => [condition, true])
);
return conditionMap;
} }
async $afterInsert(queryContext) { cannot(action, subject) {
await super.$afterInsert(queryContext); const cannot = this.ability.cannot(action, subject);
await this.createUsageData(); if (cannot) throw new NotAuthorizedError();
}
async $afterFind() { return cannot;
await this.omitEnterprisePermissionsWithoutValidLicense();
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -11,6 +11,10 @@ const redisConnection = {
const actionQueue = new Queue('action', redisConnection); const actionQueue = new Queue('action', redisConnection);
process.on('SIGTERM', async () => {
await actionQueue.close();
});
actionQueue.on('error', (error) => { actionQueue.on('error', (error) => {
if (error.code === CONNECTION_REFUSED) { if (error.code === CONNECTION_REFUSED) {
logger.error( logger.error(

View File

@@ -11,6 +11,10 @@ const redisConnection = {
const deleteUserQueue = new Queue('delete-user', redisConnection); const deleteUserQueue = new Queue('delete-user', redisConnection);
process.on('SIGTERM', async () => {
await deleteUserQueue.close();
});
deleteUserQueue.on('error', (error) => { deleteUserQueue.on('error', (error) => {
if (error.code === CONNECTION_REFUSED) { if (error.code === CONNECTION_REFUSED) {
logger.error( logger.error(

View File

@@ -11,6 +11,10 @@ const redisConnection = {
const emailQueue = new Queue('email', redisConnection); const emailQueue = new Queue('email', redisConnection);
process.on('SIGTERM', async () => {
await emailQueue.close();
});
emailQueue.on('error', (error) => { emailQueue.on('error', (error) => {
if (error.code === CONNECTION_REFUSED) { if (error.code === CONNECTION_REFUSED) {
logger.error( logger.error(

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