Compare commits

..

1 Commits

Author SHA1 Message Date
Jakub P.
d31309a92d test: add delete flow tests 2024-10-16 23:33:43 +02:00
246 changed files with 18584 additions and 24060 deletions

View File

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

View File

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

View File

@@ -18,13 +18,11 @@ jobs:
with:
node-version: '18'
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 workflow is now ready to test your code on the runner."
- run: yarn --frozen-lockfile
working-directory: packages/backend
- run: yarn lint
working-directory: packages/backend
- run: cd packages/backend && yarn lint
- run: echo "🍏 This job's status is ${{ job.status }}."
start-backend-server:
runs-on: ubuntu-latest
@@ -37,13 +35,11 @@ jobs:
with:
node-version: '18'
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 workflow is now ready to test your code on the runner."
- run: yarn --frozen-lockfile
working-directory: packages/backend
- run: yarn start
working-directory: packages/backend
- run: yarn --frozen-lockfile && yarn lerna bootstrap
- run: cd packages/backend && yarn start
env:
ENCRYPTION_KEY: sample_encryption_key
WEBHOOK_SECRET_KEY: sample_webhook_secret_key
@@ -59,13 +55,11 @@ jobs:
with:
node-version: '18'
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 workflow is now ready to test your code on the runner."
- run: yarn --frozen-lockfile
working-directory: packages/backend
- run: yarn start:worker
working-directory: packages/backend
- run: yarn --frozen-lockfile && yarn lerna bootstrap
- run: cd packages/backend && yarn start:worker
env:
ENCRYPTION_KEY: sample_encryption_key
WEBHOOK_SECRET_KEY: sample_webhook_secret_key
@@ -81,13 +75,11 @@ jobs:
with:
node-version: '18'
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 workflow is now ready to test your code on the runner."
- run: yarn --frozen-lockfile
working-directory: packages/web
- run: yarn build
working-directory: packages/web
- run: yarn --frozen-lockfile && yarn lerna bootstrap
- run: cd packages/web && yarn build
env:
CI: false
- run: echo "🍏 This job's status is ${{ job.status }}."

View File

@@ -3,7 +3,6 @@ on:
push:
branches:
- main
# TODO: Add pull request after optimizing the total excecution time of the test suite.
pull_request:
paths:
- 'packages/backend/**'
@@ -13,6 +12,9 @@ on:
workflow_dispatch:
env:
BULLMQ_DASHBOARD_USERNAME: root
BULLMQ_DASHBOARD_PASSWORD: sample
ENABLE_BULLMQ_DASHBOARD: true
ENCRYPTION_KEY: sample_encryption_key
WEBHOOK_SECRET_KEY: sample_webhook_secret_key
APP_SECRET_KEY: sample_app_secret_key
@@ -23,6 +25,7 @@ env:
POSTGRES_PASSWORD: automatisch_password
REDIS_HOST: localhost
APP_ENV: production
PORT: 3000
LICENSE_KEY: dummy_license_key
jobs:
@@ -59,21 +62,13 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: 18
- name: Install web dependencies
run: yarn
working-directory: ./packages/web
- name: Install backend dependencies
run: yarn
working-directory: ./packages/backend
- name: Install e2e-tests dependencies
run: yarn
working-directory: ./packages/e2e-tests
- name: Install dependencies
run: yarn && yarn lerna bootstrap
- name: Install Playwright Browsers
run: yarn playwright install --with-deps
working-directory: ./packages/e2e-tests
- name: Build Automatisch web
run: yarn build
working-directory: ./packages/web
run: yarn build
env:
# Keep this until we clean up warnings in build processes
CI: false

1
.gitignore vendored
View File

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

View File

@@ -11,12 +11,10 @@ WORKDIR /automatisch
# copy the app, note .dockerignore
COPY . /automatisch
RUN cd packages/web && yarn
RUN yarn
RUN cd packages/web && yarn build
RUN cd packages/backend && yarn --production
RUN \
rm -rf /usr/local/share/.cache/ && \
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",
"test": "APP_ENV=test vitest run",
"test:watch": "APP_ENV=test vitest watch",
"test:coverage": "yarn test --coverage",
"lint": "eslint .",
"db:create": "node ./bin/database/create.js",
"db:seed:user": "node ./bin/database/seed-user.js",
@@ -24,7 +23,6 @@
"dependencies": {
"@bull-board/express": "^3.10.1",
"@casl/ability": "^6.5.0",
"@faker-js/faker": "^9.2.0",
"@node-saml/passport-saml": "^4.0.4",
"@rudderstack/rudder-sdk-node": "^1.1.2",
"@sentry/node": "^7.42.0",
@@ -38,9 +36,6 @@
"crypto-js": "^4.1.1",
"debug": "~2.6.9",
"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-async-errors": "^3.1.1",
"express-basic-auth": "^1.2.1",
@@ -66,7 +61,6 @@
"pg": "^8.7.1",
"php-serialize": "^4.0.2",
"pluralize": "^8.0.0",
"prettier": "^2.5.1",
"raw-body": "^2.5.2",
"showdown": "^2.1.0",
"uuid": "^9.0.1",
@@ -98,11 +92,10 @@
"url": "https://github.com/automatisch/automatisch/issues"
},
"devDependencies": {
"@vitest/coverage-v8": "^2.1.5",
"node-gyp": "^10.1.0",
"nodemon": "^2.0.13",
"supertest": "^6.3.3",
"vitest": "^2.1.5"
"vitest": "^1.1.3"
},
"publishConfig": {
"access": "public"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -32,7 +32,7 @@ describe('GET /api/v1/admin/apps/:appKey/auth-clients/:appAuthClientId', () => {
.expect(200);
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 () => {

View File

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

View File

@@ -8,19 +8,17 @@ export default async (request, response) => {
})
.throwIfNotFound();
await appConfig.$query().patchAndFetch({
...appConfigParams(request),
key: request.params.appKey,
});
await appConfig.$query().patchAndFetch(appConfigParams(request));
renderObject(response, appConfig);
};
const appConfigParams = (request) => {
const { useOnlyPredefinedAuthClients, disabled } = request.body;
const { allowCustomConnection, shared, disabled } = request.body;
return {
useOnlyPredefinedAuthClients,
allowCustomConnection,
shared,
disabled,
};
};

View File

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

View File

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

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,
]);
expect(response.body).toStrictEqual(expectedPayload);
expect(response.body).toEqual(expectedPayload);
});
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]);
expect(response.body).toStrictEqual(expectedPayload);
expect(response.body).toEqual(expectedPayload);
});
});

View File

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

View File

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

View File

@@ -8,14 +8,15 @@ export default async (request, response) => {
.findById(samlAuthProviderId)
.throwIfNotFound();
const roleMappings = await samlAuthProvider.updateRoleMappings(
roleMappingsParams(request)
);
const samlAuthProvidersRoleMappings =
await samlAuthProvider.updateRoleMappings(
samlAuthProvidersRoleMappingsParams(request)
);
renderObject(response, roleMappings);
renderObject(response, samlAuthProvidersRoleMappings);
};
const roleMappingsParams = (request) => {
const samlAuthProvidersRoleMappingsParams = (request) => {
const roleMappings = request.body;
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 { createUser } from '../../../../../../test/factories/user.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 * 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();
await createRoleMapping({
await createSamlAuthProvidersRoleMapping({
samlAuthProviderId: samlAuthProvider.id,
remoteRoleName: 'Viewer',
});
await createRoleMapping({
await createSamlAuthProvidersRoleMapping({
samlAuthProviderId: samlAuthProvider.id,
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 () => {
const existingRoleMappings = await samlAuthProvider.$relatedQuery(
'roleMappings'
'samlAuthProvidersRoleMappings'
);
expect(existingRoleMappings.length).toBe(2);
@@ -149,4 +149,34 @@ describe('PATCH /api/v1/admin/saml-auth-providers/:samlAuthProviderId/role-mappi
.send(roleMappings)
.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);
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 () => {

View File

@@ -40,6 +40,6 @@ describe('GET /api/v1/admin/users', () => {
[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)
.expect(422);
expect(response.body.meta.type).toStrictEqual('ModelValidation');
expect(response.body.meta.type).toEqual('ModelValidation');
expect(response.body.errors).toMatchObject({
email: ['must be string'],
fullName: ['must be string'],

View File

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

View File

@@ -29,7 +29,7 @@ describe('GET /api/v1/apps/:appKey/actions/:actionKey/substeps', () => {
.expect(200);
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 () => {
@@ -47,6 +47,6 @@ describe('GET /api/v1/apps/:appKey/actions/:actionKey/substeps', () => {
.set('Authorization', token)
.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);
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 () => {

View File

@@ -23,7 +23,7 @@ describe('GET /api/v1/apps/:appKey', () => {
.expect(200);
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 () => {

View File

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

View File

@@ -37,6 +37,6 @@ describe('GET /api/v1/apps/:appKey/auth-clients', () => {
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);
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 () => {

View File

@@ -17,7 +17,8 @@ describe('GET /api/v1/apps/:appKey/config', () => {
appConfig = await createAppConfig({
key: 'deepl',
useOnlyPredefinedAuthClients: false,
allowCustomConnection: true,
shared: true,
disabled: false,
});
@@ -31,7 +32,7 @@ describe('GET /api/v1/apps/:appKey/config', () => {
.expect(200);
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 () => {

View File

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

View File

@@ -62,7 +62,7 @@ describe('GET /api/v1/apps/:appKey/flows', () => {
[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 () => {
@@ -110,7 +110,7 @@ describe('GET /api/v1/apps/:appKey/flows', () => {
[triggerStepFlowOne, actionStepFlowOne]
);
expect(response.body).toStrictEqual(expectedPayload);
expect(response.body).toEqual(expectedPayload);
});
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);
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 () => {
@@ -47,6 +47,6 @@ describe('GET /api/v1/apps/:appKey/triggers/:triggerKey/substeps', () => {
.set('Authorization', token)
.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);
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 () => {

View File

@@ -20,6 +20,6 @@ describe('GET /api/v1/automatisch/info', () => {
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();
expect(response.body).toStrictEqual(expectedPayload);
expect(response.body).toEqual(expectedPayload);
});
});

View File

@@ -10,7 +10,7 @@ describe('GET /api/v1/automatisch/version', () => {
const expectedPayload = {
data: {
version: '0.14.0',
version: '0.13.1',
},
meta: {
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]
);
expect(response.body).toStrictEqual(expectedPayload);
expect(response.body).toEqual(expectedPayload);
});
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]
);
expect(response.body).toStrictEqual(expectedPayload);
expect(response.body).toEqual(expectedPayload);
});
});

View File

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

View File

@@ -43,7 +43,7 @@ describe('POST /api/v1/connections/:connectionId/test', () => {
.set('Authorization', token)
.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 () => {
@@ -74,7 +74,7 @@ describe('POST /api/v1/connections/:connectionId/test', () => {
.set('Authorization', token)
.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 () => {

View File

@@ -8,7 +8,7 @@ export default async (request, response) => {
})
.throwIfNotFound();
connection = await connection.updateFormattedData(connectionParams(request));
connection = await connection.update(connectionParams(request));
renderObject(response, connection);
};

View File

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

View File

@@ -47,7 +47,7 @@ describe('POST /api/v1/connections/:connectionId/verify', () => {
.set('Authorization', token)
.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 () => {

View File

@@ -69,7 +69,7 @@ describe('GET /api/v1/executions/:executionId/execution-steps', () => {
[stepOne, stepTwo]
);
expect(response.body).toStrictEqual(expectedPayload);
expect(response.body).toEqual(expectedPayload);
});
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]
);
expect(response.body).toStrictEqual(expectedPayload);
expect(response.body).toEqual(expectedPayload);
});
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]
);
expect(response.body).toStrictEqual(expectedPayload);
expect(response.body).toEqual(expectedPayload);
});
it('should return the execution data of another user', async () => {
@@ -99,7 +99,7 @@ describe('GET /api/v1/executions/:executionId', () => {
[stepOne, stepTwo]
);
expect(response.body).toStrictEqual(expectedPayload);
expect(response.body).toEqual(expectedPayload);
});
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]
);
expect(response.body).toStrictEqual(expectedPayload);
expect(response.body).toEqual(expectedPayload);
});
it('should return the executions of another user', async () => {
@@ -114,6 +114,6 @@ describe('GET /api/v1/executions', () => {
[stepOne, stepTwo]
);
expect(response.body).toStrictEqual(expectedPayload);
expect(response.body).toEqual(expectedPayload);
});
});

View File

@@ -1,11 +1,11 @@
import { renderObject } from '../../../../helpers/renderer.js';
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',
});
await flow.createInitialSteps();
flow = await flow.createInitialSteps();
renderObject(response, flow, { status: 201 });
};

View File

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

View File

@@ -41,7 +41,7 @@ describe('GET /api/v1/flows/:flowId', () => {
actionStep,
]);
expect(response.body).toStrictEqual(expectedPayload);
expect(response.body).toEqual(expectedPayload);
});
it('should return the flow data of another user', async () => {
@@ -67,7 +67,7 @@ describe('GET /api/v1/flows/:flowId', () => {
actionStep,
]);
expect(response.body).toStrictEqual(expectedPayload);
expect(response.body).toEqual(expectedPayload);
});
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 () => {
@@ -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();
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();
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();
expect(response.body).toStrictEqual(expectedResponsePayload);
expect(response.body).toEqual(expectedResponsePayload);
});
});

View File

@@ -25,6 +25,6 @@ describe('GET /api/v1/saml-auth-providers', () => {
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(response.body.data).toStrictEqual(repositories);
expect(response.body.data).toEqual(repositories);
});
it('of the another users step', async () => {
@@ -117,7 +117,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-data', () => {
})
.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(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();
await request(app)
.post(`/api/v1/steps/${notExistingStepUUID}/dynamic-data`)
.get(`/api/v1/steps/${notExistingStepUUID}/dynamic-data`)
.set('Authorization', token)
.expect(404);
});
@@ -216,7 +216,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-data', () => {
const step = await createStep({ appKey: null });
await request(app)
.post(`/api/v1/steps/${step.id}/dynamic-data`)
.get(`/api/v1/steps/${step.id}/dynamic-data`)
.set('Authorization', token)
.expect(404);
});

View File

@@ -56,7 +56,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-fields', () => {
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 () => {
@@ -97,7 +97,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-fields', () => {
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 () => {
@@ -118,7 +118,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-fields', () => {
const notExistingStepUUID = Crypto.randomUUID();
await request(app)
.post(`/api/v1/steps/${notExistingStepUUID}/dynamic-fields`)
.get(`/api/v1/steps/${notExistingStepUUID}/dynamic-fields`)
.set('Authorization', token)
.expect(404);
});
@@ -138,11 +138,10 @@ describe('POST /api/v1/steps/:stepId/dynamic-fields', () => {
conditions: [],
});
const step = await createStep();
await step.$query().patch({ appKey: null });
const step = await createStep({ appKey: null });
await request(app)
.post(`/api/v1/steps/${step.id}/dynamic-fields`)
.get(`/api/v1/steps/${step.id}/dynamic-fields`)
.set('Authorization', token)
.expect(404);
});

View File

@@ -43,7 +43,7 @@ describe('GET /api/v1/steps/:stepId/connection', () => {
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 () => {
@@ -70,7 +70,7 @@ describe('GET /api/v1/steps/:stepId/connection', () => {
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 () => {

View File

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

View File

@@ -39,6 +39,6 @@ describe('GET /api/v1/users/me', () => {
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);
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 () => {
@@ -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);
expect(response.body).toStrictEqual(expectedPayload);
expect(response.body).toEqual(expectedPayload);
});
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);
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)
.expect(422);
expect(response.body.meta.type).toStrictEqual('ValidationError');
expect(response.body.meta.type).toEqual('ValidationError');
expect(response.body.errors).toMatchObject({
currentPassword: ['is incorrect.'],
});

View File

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

View File

@@ -1,37 +0,0 @@
export async function up(knex) {
await knex.schema.alterTable('app_configs', (table) => {
table.boolean('connection_allowed').defaultTo(false);
});
const appConfigs = await knex('app_configs').select('*');
for (const appConfig of appConfigs) {
const appAuthClients = await knex('app_auth_clients').where(
'app_key',
appConfig.key
);
const hasSomeActiveAppAuthClients = !!appAuthClients?.some(
(appAuthClient) => appAuthClient.active
);
const shared = appConfig.shared;
const active = appConfig.disabled === false;
const connectionAllowedConditions = [
hasSomeActiveAppAuthClients,
shared,
active,
];
const connectionAllowed = connectionAllowedConditions.every(Boolean);
await knex('app_configs')
.where('id', appConfig.id)
.update({ connection_allowed: connectionAllowed });
}
}
export async function down(knex) {
await knex.schema.alterTable('app_configs', (table) => {
table.dropColumn('connection_allowed');
});
}

View File

@@ -1,11 +0,0 @@
export async function up(knex) {
return knex.schema.alterTable('app_configs', (table) => {
table.renameColumn('allow_custom_connection', 'custom_connection_allowed');
});
}
export async function down(knex) {
return knex.schema.alterTable('app_configs', (table) => {
table.renameColumn('custom_connection_allowed', 'allow_custom_connection');
});
}

View File

@@ -1,13 +0,0 @@
export async function up(knex) {
return knex.schema.alterTable('app_configs', function (table) {
table.dropPrimary();
table.primary('key');
});
}
export async function down(knex) {
return knex.schema.alterTable('app_configs', function (table) {
table.dropPrimary();
table.primary('id');
});
}

View File

@@ -1,11 +0,0 @@
export async function up(knex) {
return knex.schema.alterTable('app_configs', function (table) {
table.dropColumn('id');
});
}
export async function down(knex) {
return knex.schema.alterTable('app_configs', function (table) {
table.uuid('id').defaultTo(knex.raw('gen_random_uuid()'));
});
}

View File

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

View File

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

View File

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

View File

@@ -30,7 +30,7 @@ const findOrCreateUserBySamlIdentity = async (
: [mappedUser.role];
const samlAuthProviderRoleMapping = await samlAuthProvider
.$relatedQuery('roleMappings')
.$relatedQuery('samlAuthProvidersRoleMappings')
.whereIn('remote_role_name', mappedRoles)
.limit(1)
.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,33 +0,0 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`AppConfig model > jsonSchema should have correct validations 1`] = `
{
"properties": {
"createdAt": {
"type": "string",
},
"disabled": {
"default": false,
"type": "boolean",
},
"id": {
"format": "uuid",
"type": "string",
},
"key": {
"type": "string",
},
"updatedAt": {
"type": "string",
},
"useOnlyPredefinedAuthClients": {
"default": false,
"type": "boolean",
},
},
"required": [
"key",
],
"type": "object",
}
`;

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,42 +0,0 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Permission model > jsonSchema should have correct validations 1`] = `
{
"properties": {
"action": {
"minLength": 1,
"type": "string",
},
"conditions": {
"items": {
"type": "string",
},
"type": "array",
},
"createdAt": {
"type": "string",
},
"id": {
"format": "uuid",
"type": "string",
},
"roleId": {
"format": "uuid",
"type": "string",
},
"subject": {
"minLength": 1,
"type": "string",
},
"updatedAt": {
"type": "string",
},
},
"required": [
"roleId",
"action",
"subject",
],
"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
exports[`RoleMapping model > jsonSchema should have the correct schema 1`] = `
exports[`SamlAuthProvidersRoleMapping model > jsonSchema should have the correct schema 1`] = `
{
"properties": {
"id": {
@@ -28,3 +28,14 @@ exports[`RoleMapping model > jsonSchema should have the correct schema 1`] = `
"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

@@ -2,7 +2,6 @@ import AES from 'crypto-js/aes.js';
import enc from 'crypto-js/enc-utf8.js';
import appConfig from '../config/app.js';
import Base from './base.js';
import AppConfig from './app-config.js';
class AppAuthClient extends Base {
static tableName = 'app_auth_clients';
@@ -22,17 +21,6 @@ class AppAuthClient extends Base {
},
};
static relationMappings = () => ({
appConfig: {
relation: Base.BelongsToOneRelation,
modelClass: AppConfig,
join: {
from: 'app_auth_clients.app_key',
to: 'app_configs.key',
},
},
});
encryptData() {
if (!this.eligibleForEncryption()) return;
@@ -64,24 +52,14 @@ class AppAuthClient extends Base {
// beforeInsert and beforeUpdate separately for the same operation.
async $beforeInsert(queryContext) {
await super.$beforeInsert(queryContext);
this.encryptData();
}
async $afterInsert(queryContext) {
await super.$afterInsert(queryContext);
}
async $beforeUpdate(opt, queryContext) {
await super.$beforeUpdate(opt, queryContext);
this.encryptData();
}
async $afterUpdate(opt, queryContext) {
await super.$afterUpdate(opt, queryContext);
}
async $afterFind() {
this.decryptData();
}

View File

@@ -2,9 +2,7 @@ import { describe, it, expect, vi } from 'vitest';
import AES from 'crypto-js/aes.js';
import enc from 'crypto-js/enc-utf8.js';
import AppConfig from './app-config.js';
import AppAuthClient from './app-auth-client.js';
import Base from './base.js';
import appConfig from '../config/app.js';
import { createAppAuthClient } from '../../test/factories/app-auth-client.js';
@@ -17,23 +15,6 @@ describe('AppAuthClient model', () => {
expect(AppAuthClient.jsonSchema).toMatchSnapshot();
});
it('relationMappings should return correct associations', () => {
const relationMappings = AppAuthClient.relationMappings();
const expectedRelations = {
appConfig: {
relation: Base.BelongsToOneRelation,
modelClass: AppConfig,
join: {
from: 'app_auth_clients.app_key',
to: 'app_configs.key',
},
},
};
expect(relationMappings).toStrictEqual(expectedRelations);
});
describe('encryptData', () => {
it('should return undefined if eligibleForEncryption is not true', async () => {
vi.spyOn(
@@ -68,9 +49,7 @@ describe('AppAuthClient model', () => {
);
expect(formattedAuthDefaults).toStrictEqual(expectedDecryptedValue);
expect(appAuthClient.authDefaults).not.toStrictEqual(
formattedAuthDefaults
);
expect(appAuthClient.authDefaults).not.toEqual(formattedAuthDefaults);
});
it('should encrypt formattedAuthDefaults and remove formattedAuthDefaults', async () => {
@@ -125,9 +104,7 @@ describe('AppAuthClient model', () => {
expect(appAuthClient.formattedAuthDefaults).toStrictEqual(
formattedAuthDefaults
);
expect(appAuthClient.authDefaults).not.toStrictEqual(
formattedAuthDefaults
);
expect(appAuthClient.authDefaults).not.toEqual(formattedAuthDefaults);
});
});

View File

@@ -5,10 +5,6 @@ import Base from './base.js';
class AppConfig extends Base {
static tableName = 'app_configs';
static get idColumn() {
return 'key';
}
static jsonSchema = {
type: 'object',
required: ['key'],
@@ -16,7 +12,8 @@ class AppConfig extends Base {
properties: {
id: { type: 'string', format: 'uuid' },
key: { type: 'string' },
useOnlyPredefinedAuthClients: { type: 'boolean', default: false },
allowCustomConnection: { type: 'boolean', default: false },
shared: { type: 'boolean', default: false },
disabled: { type: 'boolean', default: false },
createdAt: { type: 'string' },
updatedAt: { type: 'string' },
@@ -34,6 +31,26 @@ class AppConfig extends Base {
},
});
static get virtualAttributes() {
return ['canConnect', 'canCustomConnect'];
}
get canCustomConnect() {
return !this.disabled && this.allowCustomConnection;
}
get canConnect() {
const hasSomeActiveAppAuthClients = !!this.appAuthClients?.some(
(appAuthClient) => appAuthClient.active
);
const shared = this.shared;
const active = this.disabled === false;
const conditions = [hasSomeActiveAppAuthClients, shared, active];
return conditions.every(Boolean);
}
async getApp() {
if (!this.key) return null;

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