Compare commits

..

1 Commits

Author SHA1 Message Date
kasia.oczkowska
b48b2592d5 feat: show api error message when logging in fails 2024-11-13 14:46:21 +00:00
60 changed files with 17857 additions and 19693 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,35 +21,24 @@ 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
echo "Done!"
echo "Done!"

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

@@ -55,44 +55,19 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v4
- uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'yarn'
cache-dependency-path: |
packages/backend/yarn.lock
packages/web/yarn.lock
packages/e2e-tests/yarn.lock
- name: Install backend dependencies
run: yarn --frozen-lockfile
working-directory: ./packages/backend
- name: Install web dependencies
run: yarn --frozen-lockfile
working-directory: ./packages/web
- name: Install e2e-tests dependencies
run: yarn --frozen-lockfile
working-directory: ./packages/e2e-tests
- name: Get installed Playwright version
id: playwright-version
run: echo "PLAYWRIGHT_VERSION=$(node -e "console.log(require('./package.json').devDependencies['@playwright/test'])")" >> $GITHUB_ENV
working-directory: ./packages/e2e-tests
- name: Cache playwright binaries
uses: actions/cache@v3
id: playwright-cache
with:
path: |
~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }}
node-version: 18
- name: Install dependencies
run: yarn && yarn lerna bootstrap
- name: Install Playwright Browsers
run: yarn playwright install --with-deps
working-directory: ./packages/e2e-tests
if: steps.playwright-cache.outputs.cache-hit != 'true'
- name: Build Automatisch web
working-directory: ./packages/web
run: yarn build
env:
# Keep this until we clean up warnings in build processes
CI: false
working-directory: ./packages/web
- name: Migrate database
working-directory: ./packages/backend
run: yarn db:migrate
@@ -132,7 +107,6 @@ jobs:
env:
LOGIN_EMAIL: user@automatisch.io
LOGIN_PASSWORD: sample
BACKEND_APP_URL: http://localhost:3000
BASE_URL: http://localhost:3000
GITHUB_CLIENT_ID: 1c0417daf898adfbd99a
GITHUB_CLIENT_SECRET: 3328fa814dd582ccd03dbe785cfd683fb8da92b3

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

@@ -23,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",
@@ -37,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",
@@ -65,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",

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

@@ -10,7 +10,7 @@ describe('GET /api/v1/automatisch/version', () => {
const expectedPayload = {
data: {
version: '0.14.0',
version: '0.13.1',
},
meta: {
count: 1,

View File

@@ -1,9 +1,8 @@
import { vi, describe, it, expect } from 'vitest';
import { describe, it, expect } from 'vitest';
import SamlAuthProvider from '../models/saml-auth-provider.ee';
import SamlAuthProvidersRoleMapping from '../models/saml-auth-providers-role-mapping.ee';
import Identity from './identity.ee';
import Base from './base';
import appConfig from '../config/app';
describe('SamlAuthProvider model', () => {
it('tableName should return correct name', () => {
@@ -46,39 +45,4 @@ describe('SamlAuthProvider model', () => {
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'
);
});
});

View File

@@ -407,7 +407,7 @@ class User extends Base {
}
}
startTrialPeriod() {
async startTrialPeriod() {
this.trialExpiryDate = DateTime.now().plus({ days: 30 }).toISODate();
}
@@ -590,7 +590,7 @@ class User extends Base {
await this.generateHash();
if (appConfig.isCloud) {
this.startTrialPeriod();
await this.startTrialPeriod();
}
}

View File

@@ -1,9 +1,7 @@
import { describe, it, expect, vi } from 'vitest';
import { DateTime, Duration } from 'luxon';
import appConfig from '../config/app.js';
import Base from './base.js';
import AccessToken from './access-token.js';
import Config from './config.js';
import Connection from './connection.js';
import Execution from './execution.js';
import Flow from './flow.js';
@@ -14,12 +12,6 @@ import Step from './step.js';
import Subscription from './subscription.ee.js';
import UsageData from './usage-data.ee.js';
import User from './user.js';
import deleteUserQueue from '../queues/delete-user.ee.js';
import emailQueue from '../queues/email.js';
import {
REMOVE_AFTER_30_DAYS_OR_150_JOBS,
REMOVE_AFTER_7_DAYS_OR_50_JOBS,
} from '../helpers/remove-job-configuration.js';
import { createUser } from '../../test/factories/user.js';
import { createConnection } from '../../test/factories/connection.js';
import { createRole } from '../../test/factories/role.js';
@@ -27,9 +19,6 @@ import { createPermission } from '../../test/factories/permission.js';
import { createFlow } from '../../test/factories/flow.js';
import { createStep } from '../../test/factories/step.js';
import { createExecution } from '../../test/factories/execution.js';
import { createSubscription } from '../../test/factories/subscription.js';
import { createUsageData } from '../../test/factories/usage-data.js';
import Billing from '../helpers/billing/index.ee.js';
describe('User model', () => {
it('tableName should return correct name', () => {
@@ -591,597 +580,4 @@ describe('User model', () => {
expect(refetchedUser.invitationTokenSentAt).toBe(null);
expect(refetchedUser.status).toBe('active');
});
describe('updatePassword', () => {
it('should update password when the given current password matches with the user password', async () => {
const user = await createUser({ password: 'sample-password' });
const updatedUser = await user.updatePassword({
currentPassword: 'sample-password',
password: 'new-password',
});
expect(await updatedUser.login('new-password')).toBe(true);
});
it('should throw validation error when the given current password does not match with the user password', async () => {
const user = await createUser({ password: 'sample-password' });
await expect(
user.updatePassword({
currentPassword: 'wrong-password',
password: 'new-password',
})
).rejects.toThrowError('currentPassword: is incorrect.');
});
});
it('softRemove should soft remove the user, its associations and queue it for hard deletion in 30 days', async () => {
vi.useFakeTimers();
const date = new Date(2024, 10, 12, 12, 50, 0, 0);
vi.setSystemTime(date);
const user = await createUser();
const softRemoveAssociationsSpy = vi
.spyOn(user, 'softRemoveAssociations')
.mockReturnValue();
const deleteUserQueueAddSpy = vi
.spyOn(deleteUserQueue, 'add')
.mockResolvedValue();
await user.softRemove();
const refetchedSoftDeletedUser = await user.$query().withSoftDeleted();
const millisecondsFor30Days = Duration.fromObject({ days: 30 }).toMillis();
const jobName = `Delete user - ${user.id}`;
const jobPayload = { id: user.id };
const jobOptions = {
delay: millisecondsFor30Days,
};
expect(softRemoveAssociationsSpy).toHaveBeenCalledOnce();
expect(refetchedSoftDeletedUser.deletedAt).toStrictEqual(date);
expect(deleteUserQueueAddSpy).toHaveBeenCalledWith(
jobName,
jobPayload,
jobOptions
);
vi.useRealTimers();
});
it.todo('softRemoveAssociations');
it('sendResetPasswordEmail should generate reset password token and queue to send reset password email', async () => {
vi.useFakeTimers();
const date = new Date(2024, 10, 12, 14, 33, 0, 0);
vi.setSystemTime(date);
const user = await createUser();
const generateResetPasswordTokenSpy = vi
.spyOn(user, 'generateResetPasswordToken')
.mockReturnValue();
const emailQueueAddSpy = vi.spyOn(emailQueue, 'add').mockResolvedValue();
await user.sendResetPasswordEmail();
const refetchedUser = await user.$query();
const jobName = `Reset Password Email - ${user.id}`;
const jobPayload = {
email: refetchedUser.email,
subject: 'Reset Password',
template: 'reset-password-instructions.ee',
params: {
token: refetchedUser.resetPasswordToken,
webAppUrl: appConfig.webAppUrl,
fullName: refetchedUser.fullName,
},
};
const jobOptions = {
removeOnComplete: REMOVE_AFTER_7_DAYS_OR_50_JOBS,
removeOnFail: REMOVE_AFTER_30_DAYS_OR_150_JOBS,
};
expect(generateResetPasswordTokenSpy).toHaveBeenCalledOnce();
expect(emailQueueAddSpy).toHaveBeenCalledWith(
jobName,
jobPayload,
jobOptions
);
vi.useRealTimers();
});
describe('isResetPasswordTokenValid', () => {
it('should return true when resetPasswordTokenSentAt is within the next four hours', async () => {
vi.useFakeTimers();
const date = DateTime.fromObject(
{ year: 2024, month: 11, day: 12, hour: 16, minute: 30 },
{ zone: 'UTC+0' }
);
vi.setSystemTime(date);
const user = new User();
user.resetPasswordTokenSentAt = '2024-11-12T13:31:00.000Z';
expect(user.isResetPasswordTokenValid()).toBe(true);
vi.useRealTimers();
});
it('should return false when there is no resetPasswordTokenSentAt', async () => {
const user = new User();
expect(user.isResetPasswordTokenValid()).toBe(false);
});
it('should return false when resetPasswordTokenSentAt is older than four hours', async () => {
vi.useFakeTimers();
const date = DateTime.fromObject(
{ year: 2024, month: 11, day: 12, hour: 16, minute: 30 },
{ zone: 'UTC+0' }
);
vi.setSystemTime(date);
const user = new User();
user.resetPasswordTokenSentAt = '2024-11-12T12:29:00.000Z';
expect(user.isResetPasswordTokenValid()).toBe(false);
vi.useRealTimers();
});
});
it('sendInvitationEmail should generate invitation token and queue to send invitation email', async () => {
vi.useFakeTimers();
const date = DateTime.fromObject(
{ year: 2024, month: 11, day: 12, hour: 17, minute: 10 },
{ zone: 'UTC+0' }
);
vi.setSystemTime(date);
const user = await createUser();
const generateInvitationTokenSpy = vi
.spyOn(user, 'generateInvitationToken')
.mockReturnValue();
const emailQueueAddSpy = vi.spyOn(emailQueue, 'add').mockResolvedValue();
await user.sendInvitationEmail();
const refetchedUser = await user.$query();
const jobName = `Invitation Email - ${refetchedUser.id}`;
const jobPayload = {
email: refetchedUser.email,
subject: 'You are invited!',
template: 'invitation-instructions',
params: {
fullName: refetchedUser.fullName,
acceptInvitationUrl: refetchedUser.acceptInvitationUrl,
},
};
const jobOptions = {
removeOnComplete: REMOVE_AFTER_7_DAYS_OR_50_JOBS,
removeOnFail: REMOVE_AFTER_30_DAYS_OR_150_JOBS,
};
expect(generateInvitationTokenSpy).toHaveBeenCalledOnce();
expect(emailQueueAddSpy).toHaveBeenCalledWith(
jobName,
jobPayload,
jobOptions
);
vi.useRealTimers();
});
describe('isInvitationTokenValid', () => {
it('should return truen when invitationTokenSentAt is within the next four hours', async () => {
vi.useFakeTimers();
const date = DateTime.fromObject(
{ year: 2024, month: 11, day: 14, hour: 14, minute: 30 },
{ zone: 'UTC+0' }
);
vi.setSystemTime(date);
const user = new User();
user.invitationTokenSentAt = '2024-11-14T13:31:00.000Z';
expect(user.isInvitationTokenValid()).toBe(true);
vi.useRealTimers();
});
it('should return false when there is no invitationTokenSentAt', async () => {
const user = new User();
expect(user.isInvitationTokenValid()).toBe(false);
});
it('should return false when invitationTokenSentAt is older than seventy two hours', async () => {
vi.useFakeTimers();
const date = DateTime.fromObject(
{ year: 2024, month: 11, day: 14, hour: 14, minute: 30 },
{ zone: 'UTC+0' }
);
vi.setSystemTime(date);
const user = new User();
user.invitationTokenSentAt = '2024-11-11T14:20:00.000Z';
expect(user.isInvitationTokenValid()).toBe(false);
vi.useRealTimers();
});
});
describe('generateHash', () => {
it('should hash password and re-assign it', async () => {
const user = new User();
user.password = 'sample-password';
await user.generateHash();
expect(user.password).not.toBe('sample-password');
expect(await user.login('sample-password')).toBe(true);
});
it('should do nothing when password does not exist', async () => {
const user = new User();
await user.generateHash();
expect(user.password).toBe(undefined);
});
});
it('startTrialPeriod should assign trialExpiryDate 30 days from now', () => {
vi.useFakeTimers();
const date = DateTime.fromObject(
{ year: 2024, month: 11, day: 14, hour: 16 },
{ zone: 'UTC+0' }
);
vi.setSystemTime(date);
const user = new User();
user.startTrialPeriod();
expect(user.trialExpiryDate).toBe('2024-12-14');
vi.useRealTimers();
});
describe('isAllowedToRunFlows', () => {
it('should return true when Automatisch is self hosted', async () => {
const user = new User();
vi.spyOn(appConfig, 'isSelfHosted', 'get').mockReturnValue(true);
expect(await user.isAllowedToRunFlows()).toBe(true);
});
it('should return true when the user is in trial', async () => {
const user = new User();
vi.spyOn(user, 'inTrial').mockResolvedValue(true);
expect(await user.isAllowedToRunFlows()).toBe(true);
});
it('should return true when the user has active subscription and within quota limits', async () => {
const user = new User();
vi.spyOn(user, 'hasActiveSubscription').mockResolvedValue(true);
vi.spyOn(user, 'withinLimits').mockResolvedValue(true);
expect(await user.isAllowedToRunFlows()).toBe(true);
});
it('should return false when the user has active subscription over quota limits', async () => {
const user = new User();
vi.spyOn(user, 'hasActiveSubscription').mockResolvedValue(true);
vi.spyOn(user, 'withinLimits').mockResolvedValue(false);
expect(await user.isAllowedToRunFlows()).toBe(false);
});
it('should return false otherwise', async () => {
const user = new User();
expect(await user.isAllowedToRunFlows()).toBe(false);
});
});
describe('inTrial', () => {
it('should return false when Automatisch is self hosted', async () => {
const user = new User();
vi.spyOn(appConfig, 'isSelfHosted', 'get').mockReturnValue(true);
expect(await user.inTrial()).toBe(false);
});
it('should return false when the user does not have trial expiry date', async () => {
const user = new User();
vi.spyOn(appConfig, 'isSelfHosted', 'get').mockReturnValue(false);
expect(await user.inTrial()).toBe(false);
});
it('should return false when the user has an active subscription', async () => {
const user = new User();
user.trialExpiryDate = '2024-12-14';
vi.spyOn(appConfig, 'isSelfHosted', 'get').mockReturnValue(false);
const hasActiveSubscriptionSpy = vi
.spyOn(user, 'hasActiveSubscription')
.mockResolvedValue(true);
expect(await user.inTrial()).toBe(false);
expect(hasActiveSubscriptionSpy).toHaveBeenCalledOnce();
});
it('should return true when trial expiry date is in future', async () => {
vi.useFakeTimers();
const date = DateTime.fromObject(
{ year: 2024, month: 11, day: 12, hour: 17, minute: 30 },
{ zone: 'UTC+0' }
);
vi.setSystemTime(date);
const user = await createUser();
await user.startTrialPeriod();
const refetchedUser = await user.$query();
vi.spyOn(appConfig, 'isSelfHosted', 'get').mockReturnValue(false);
vi.spyOn(refetchedUser, 'hasActiveSubscription').mockResolvedValue(false);
expect(await refetchedUser.inTrial()).toBe(true);
vi.useRealTimers();
});
it('should return false when trial expiry date is in past', async () => {
vi.useFakeTimers();
const user = await createUser();
const presentDate = DateTime.fromObject(
{ year: 2024, month: 11, day: 17, hour: 11, minute: 30 },
{ zone: 'UTC+0' }
);
vi.setSystemTime(presentDate);
await user.startTrialPeriod();
const futureDate = DateTime.fromObject(
{ year: 2025, month: 1, day: 1 },
{ zone: 'UTC+0' }
);
vi.setSystemTime(futureDate);
const refetchedUser = await user.$query();
vi.spyOn(appConfig, 'isSelfHosted', 'get').mockReturnValue(false);
vi.spyOn(refetchedUser, 'hasActiveSubscription').mockResolvedValue(false);
expect(await refetchedUser.inTrial()).toBe(false);
vi.useRealTimers();
});
});
describe('hasActiveSubscription', () => {
it('should return true if current subscription is valid', async () => {
const user = await createUser();
await createSubscription({ userId: user.id, status: 'active' });
expect(await user.hasActiveSubscription()).toBe(true);
});
it('should return false if current subscription is not valid', async () => {
const user = await createUser();
await createSubscription({
userId: user.id,
status: 'deleted',
cancellationEffectiveDate: DateTime.now().minus({ day: 1 }).toString(),
});
expect(await user.hasActiveSubscription()).toBe(false);
});
it('should return false if Automatisch is not a cloud installation', async () => {
const user = new User();
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(false);
expect(await user.hasActiveSubscription()).toBe(false);
});
});
describe('withinLimits', () => {
it('should return true when the consumed task count is less than the quota', async () => {
const user = await createUser();
const subscription = await createSubscription({ userId: user.id });
await createUsageData({
subscriptionId: subscription.id,
userId: user.id,
consumedTaskCount: 100,
});
expect(await user.withinLimits()).toBe(true);
});
it('should return true when the consumed task count is less than the quota', async () => {
const user = await createUser();
const subscription = await createSubscription({ userId: user.id });
await createUsageData({
subscriptionId: subscription.id,
userId: user.id,
consumedTaskCount: 10000,
});
expect(await user.withinLimits()).toBe(false);
});
});
describe('getPlanAndUsage', () => {
it('should return plan and usage', async () => {
const user = await createUser();
const subscription = await createSubscription({ userId: user.id });
expect(await user.getPlanAndUsage()).toStrictEqual({
usage: {
task: 0,
},
plan: {
id: subscription.paddlePlanId,
name: '10k - monthly',
limit: '10,000',
},
});
});
it('should return trial plan and usage if no subscription exists', async () => {
const user = await createUser();
expect(await user.getPlanAndUsage()).toStrictEqual({
usage: {
task: 0,
},
plan: {
id: null,
name: 'Free Trial',
limit: null,
},
});
});
it('should throw not found when the current usage data does not exist', async () => {
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(false);
const user = await createUser();
expect(() => user.getPlanAndUsage()).rejects.toThrow('NotFoundError');
});
});
describe('getInvoices', () => {
it('should return invoices for the current subscription', async () => {
const user = await createUser();
const subscription = await createSubscription({ userId: user.id });
const getInvoicesSpy = vi
.spyOn(Billing.paddleClient, 'getInvoices')
.mockResolvedValue('dummy-invoices');
expect(await user.getInvoices()).toBe('dummy-invoices');
expect(getInvoicesSpy).toHaveBeenCalledWith(
Number(subscription.paddleSubscriptionId)
);
});
it('should return empty array without any subscriptions', async () => {
const user = await createUser();
expect(await user.getInvoices()).toStrictEqual([]);
});
});
it.todo('getApps');
it('createAdmin should create admin with given data and mark the installation completed', async () => {
const adminRole = await createRole({ name: 'Admin' });
const markInstallationCompletedSpy = vi
.spyOn(Config, 'markInstallationCompleted')
.mockResolvedValue();
const adminUser = await User.createAdmin({
fullName: 'Sample admin',
email: 'admin@automatisch.io',
password: 'sample',
});
expect(adminUser).toMatchObject({
fullName: 'Sample admin',
email: 'admin@automatisch.io',
roleId: adminRole.id,
});
expect(markInstallationCompletedSpy).toHaveBeenCalledOnce();
expect(await adminUser.login('sample')).toBe(true);
});
describe('registerUser', () => {
it('should register user with user role and given data', async () => {
const userRole = await createRole({ name: 'User' });
const user = await User.registerUser({
fullName: 'Sample user',
email: 'user@automatisch.io',
password: 'sample-password',
});
expect(user).toMatchObject({
fullName: 'Sample user',
email: 'user@automatisch.io',
roleId: userRole.id,
});
expect(await user.login('sample-password')).toBe(true);
});
it('should throw not found error when user role does not exist', async () => {
expect(() =>
User.registerUser({
fullName: 'Sample user',
email: 'user@automatisch.io',
password: 'sample-password',
})
).rejects.toThrowError('NotFoundError');
});
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -1 +0,0 @@
pages/.vitepress/cache

View File

@@ -4,7 +4,6 @@
"license": "See LICENSE file",
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
"private": true,
"type": "module",
"scripts": {
"dev": "vitepress dev pages --port 3002",
"build": "vitepress build pages",

View File

@@ -6,19 +6,11 @@ Clone main branch of Automatisch.
git clone git@github.com:automatisch/automatisch.git
```
Then, install the dependencies for both backend and web packages separately.
Then, install the dependencies.
```bash
cd automatisch
# Install backend dependencies
cd packages/backend
yarn install
# Install web dependencies
cd packages/web
yarn install
```
## Backend
@@ -61,14 +53,12 @@ yarn db:seed:user
Start the main backend server.
```bash
cd packages/backend
yarn dev
```
Start the worker server in another terminal tab.
```bash
cd packages/backend
yarn worker
```
@@ -94,7 +84,6 @@ It will automatically open [http://localhost:3001](http://localhost:3001) in you
```bash
cd packages/docs
yarn install
yarn dev
```

View File

@@ -1,6 +1,6 @@
# Repository Structure
We manage a monorepo structure with the following packages:
We use `lerna` with `yarn workspaces` to manage the mono repository. We have the following packages:
```
.
@@ -15,5 +15,3 @@ We manage a monorepo structure with the following packages:
- `docs` - The docs package contains the documentation website.
- `e2e-tests` - The e2e-tests package contains the end-to-end tests for the internal usage.
- `web` - The web package contains the frontend application of Automatisch.
Each package is independently managed, and has its own package.json file to manage dependencies. This allows for better isolation and flexibility.

File diff suppressed because it is too large Load Diff

View File

@@ -2,5 +2,4 @@ POSTGRES_DB=automatisch
POSTGRES_USER=automatisch_user
POSTGRES_PASSWORD=automatisch_password
POSTGRES_PORT=5432
POSTGRES_HOST=localhost
BACKEND_APP_URL=http://localhost:3000
POSTGRES_HOST=localhost

View File

@@ -20,54 +20,44 @@ export class AdminApplicationSettingsPage extends AuthenticatedPage {
}
async allowCustomConnections() {
await expect(this.allowCustomConnectionsSwitch).not.toBeChecked();
await expect(this.disableConnectionsSwitch).not.toBeChecked();
await this.allowCustomConnectionsSwitch.check();
await expect(this.allowCustomConnectionsSwitch).toBeChecked();
await this.saveButton.click();
}
async allowSharedConnections() {
await expect(this.allowSharedConnectionsSwitch).not.toBeChecked();
await expect(this.disableConnectionsSwitch).not.toBeChecked();
await this.allowSharedConnectionsSwitch.check();
await expect(this.allowSharedConnectionsSwitch).toBeChecked();
await this.saveButton.click();
}
async disallowConnections() {
await expect(this.disableConnectionsSwitch).not.toBeChecked();
await this.disableConnectionsSwitch.check();
await expect(this.disableConnectionsSwitch).toBeChecked();
await this.saveButton.click();
}
async disallowCustomConnections() {
await expect(this.allowCustomConnectionsSwitch).toBeChecked();
await expect(this.disableConnectionsSwitch).toBeChecked();
await this.allowCustomConnectionsSwitch.uncheck();
await expect(this.allowCustomConnectionsSwitch).not.toBeChecked();
await this.saveButton.click();
}
async disallowSharedConnections() {
await expect(this.allowSharedConnectionsSwitch).toBeChecked();
await expect(this.disableConnectionsSwitch).toBeChecked();
await this.allowSharedConnectionsSwitch.uncheck();
await expect(this.allowSharedConnectionsSwitch).not.toBeChecked();
await this.saveButton.click();
}
async allowConnections() {
await expect(this.disableConnectionsSwitch).toBeChecked();
await this.disableConnectionsSwitch.uncheck();
await expect(this.disableConnectionsSwitch).not.toBeChecked();
await this.saveButton.click();
}
async expectSuccessSnackbarToBeVisible() {
const snackbars = await this.successSnackbar.all();
for (const snackbar of snackbars) {
await expect(await snackbar.getAttribute('data-snackbar-variant')).toBe(
'success'
);
// await snackbar.click();
}
await expect(this.successSnackbar).toHaveCount(1);
await this.successSnackbar.click();
await expect(this.successSnackbar).toHaveCount(0);
}
}

View File

@@ -1,5 +1,3 @@
import { expect } from '@playwright/test';
const { AuthenticatedPage } = require('../authenticated-page');
const { RoleConditionsModal } = require('./role-conditions-modal');
@@ -18,7 +16,6 @@ export class AdminCreateRolePage extends AuthenticatedPage {
this.executionRow = page.getByTestId('Execution-permission-row');
this.flowRow = page.getByTestId('Flow-permission-row');
this.pageTitle = page.getByTestId('create-role-title');
this.permissionsCatalog = page.getByTestId('permissions-catalog');
}
/**
@@ -107,8 +104,4 @@ export class AdminCreateRolePage extends AuthenticatedPage {
throw new Error(`${subject} does not have action ${action}`);
}
}
async waitForPermissionsCatalogToVisible() {
await expect(this.permissionsCatalog).toBeVisible();
}
}

View File

@@ -14,12 +14,8 @@ export class AdminCreateUserPage extends AuthenticatedPage {
this.roleInput = page.getByTestId('role.id-autocomplete');
this.createButton = page.getByTestId('create-button');
this.pageTitle = page.getByTestId('create-user-title');
this.invitationEmailInfoAlert = page.getByTestId(
'invitation-email-info-alert'
);
this.acceptInvitationLink = page
.getByTestId('invitation-email-info-alert')
.getByRole('link');
this.invitationEmailInfoAlert = page.getByTestId('invitation-email-info-alert');
this.acceptInvitationLink = page.getByTestId('invitation-email-info-alert').getByRole('link');
}
seed(seed) {

View File

@@ -95,6 +95,7 @@ export class AdminUsersPage extends AuthenticatedPage {
});
}
const rowLocator = await this.getUserRowByEmail(email);
console.log('rowLocator.count', email, await rowLocator.count());
if ((await rowLocator.count()) === 1) {
return rowLocator;
}

View File

@@ -51,20 +51,10 @@ export class BasePage {
};
}
async closeSnackbar() {
await this.snackbar.click();
}
async closeSnackbarAndWaitUntilDetached() {
const snackbar = await this.snackbar;
await snackbar.click();
await snackbar.waitFor({ state: 'detached' });
}
/**
* Closes all snackbars, should be replaced later
*/
async closeAllSnackbars() {
async closeSnackbar() {
const snackbars = await this.snackbar.all();
for (const snackbar of snackbars) {
await snackbar.click();

View File

@@ -1,16 +0,0 @@
const { expect } = require('../fixtures/index');
export const getToken = async (apiRequest) => {
const tokenResponse = await apiRequest.post(
`${process.env.BACKEND_APP_URL}/api/v1/access-tokens`,
{
data: {
email: process.env.LOGIN_EMAIL,
password: process.env.LOGIN_PASSWORD,
},
}
);
await expect(tokenResponse.status()).toBe(200);
return await tokenResponse.json();
};

View File

@@ -1,69 +0,0 @@
const { expect } = require('../fixtures/index');
export const createFlow = async (request, token) => {
const response = await request.post(
`${process.env.BACKEND_APP_URL}/api/v1/flows`,
{ headers: { Authorization: token } }
);
await expect(response.status()).toBe(201);
return await response.json();
};
export const getFlow = async (request, token, flowId) => {
const response = await request.get(
`${process.env.BACKEND_APP_URL}/api/v1/flows/${flowId}`,
{ headers: { Authorization: token } }
);
await expect(response.status()).toBe(200);
return await response.json();
};
export const updateFlowName = async (request, token, flowId) => {
const updateFlowNameResponse = await request.patch(
`${process.env.BACKEND_APP_URL}/api/v1/flows/${flowId}`,
{
headers: { Authorization: token },
data: { name: flowId },
}
);
await expect(updateFlowNameResponse.status()).toBe(200);
};
export const updateFlowStep = async (request, token, stepId, requestBody) => {
const updateTriggerStepResponse = await request.patch(
`${process.env.BACKEND_APP_URL}/api/v1/steps/${stepId}`,
{
headers: { Authorization: token },
data: requestBody,
}
);
await expect(updateTriggerStepResponse.status()).toBe(200);
return await updateTriggerStepResponse.json();
};
export const testStep = async (request, token, stepId) => {
const testTriggerStepResponse = await request.post(
`${process.env.BACKEND_APP_URL}/api/v1/steps/${stepId}/test`,
{
headers: { Authorization: token },
}
);
await expect(testTriggerStepResponse.status()).toBe(200);
};
export const publishFlow = async (request, token, flowId) => {
const publishFlowResponse = await request.patch(
`${process.env.BACKEND_APP_URL}/api/v1/flows/${flowId}/status`,
{
headers: { Authorization: token },
data: { active: true },
}
);
await expect(publishFlowResponse.status()).toBe(200);
return publishFlowResponse.json();
};
export const triggerFlow = async (request, url) => {
const triggerFlowResponse = await request.get(url);
await expect(triggerFlowResponse.status()).toBe(204);
};

View File

@@ -1,24 +0,0 @@
const { expect } = require('../fixtures/index');
export const addUser = async (apiRequest, token, request) => {
const addUserResponse = await apiRequest.post(
`${process.env.BACKEND_APP_URL}/api/v1/admin/users`,
{
headers: { Authorization: token },
data: request,
}
);
await expect(addUserResponse.status()).toBe(201);
return await addUserResponse.json();
};
export const acceptInvitation = async (apiRequest, request) => {
const acceptInvitationResponse = await apiRequest.post(
`${process.env.BACKEND_APP_URL}/api/v1/users/invitation`,
{
data: request,
}
);
await expect(acceptInvitationResponse.status()).toBe(204);
};

View File

@@ -1,5 +1,3 @@
import { knexSnakeCaseMappers } from 'objection';
const fileExtension = 'js';
const knexConfig = {
@@ -9,7 +7,7 @@ const knexConfig = {
user: process.env.POSTGRES_USERNAME,
port: process.env.POSTGRES_PORT,
password: process.env.POSTGRES_PASSWORD,
database: process.env.POSTGRES_DATABASE,
database: process.env.POSTGRES_DATABASE
},
searchPath: ['public'],
pool: { min: 0, max: 20 },

View File

@@ -26,16 +26,13 @@
},
"devDependencies": {
"@faker-js/faker": "^8.2.0",
"@playwright/test": "1.49.0",
"objection": "^3.1.5"
"@playwright/test": "^1.45.1"
},
"dependencies": {
"axios": "^1.6.0",
"dotenv": "^16.3.1",
"eslint": "^8.13.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"knex": "^2.4.0",
"luxon": "^3.4.4",
"micro": "^10.0.1",
"pg": "^8.12.0",

View File

@@ -15,9 +15,9 @@ module.exports = defineConfig({
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 1 : 0,
retries: 0,
/* Opt out of parallel tests on CI. */
workers: undefined,
workers: process.env.CI ? 1 : undefined,
/* Timeout threshold for each test */
timeout: 30000,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
@@ -30,7 +30,7 @@ module.exports = defineConfig({
baseURL: process.env.BASE_URL || 'http://localhost:3001',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
trace: 'retain-on-failure',
testIdAttribute: 'data-test',
viewport: { width: 1280, height: 720 },
},

View File

@@ -5,18 +5,16 @@ test.describe('Admin Applications', () => {
test.beforeAll(async () => {
const deleteAppAuthClients = {
text: 'DELETE FROM app_auth_clients WHERE app_key in ($1, $2, $3, $4, $5)',
values: ['carbone', 'spotify', 'deepl', 'mailchimp', 'reddit'],
values: ['carbone', 'spotify', 'deepl', 'mailchimp', 'reddit']
};
const deleteAppConfigs = {
text: 'DELETE FROM app_configs WHERE key in ($1, $2, $3, $4, $5)',
values: ['carbone', 'spotify', 'deepl', 'mailchimp', 'reddit'],
values: ['carbone', 'spotify', 'deepl', 'mailchimp', 'reddit']
};
try {
const deleteAppAuthClientsResult = await pgPool.query(
deleteAppAuthClients
);
const deleteAppAuthClientsResult = await pgPool.query(deleteAppAuthClients);
expect(deleteAppAuthClientsResult.command).toBe('DELETE');
const deleteAppConfigsResult = await pgPool.query(deleteAppConfigs);
expect(deleteAppConfigsResult.command).toBe('DELETE');
@@ -30,11 +28,10 @@ test.describe('Admin Applications', () => {
await adminApplicationsPage.navigateTo();
});
// TODO skip until https://github.com/automatisch/automatisch/pull/2244
test.skip('Admin should be able to toggle Application settings', async ({
test('Admin should be able to toggle Application settings', async ({
adminApplicationsPage,
adminApplicationSettingsPage,
page,
page
}) => {
await adminApplicationsPage.openApplication('Carbone');
await expect(page.url()).toContain('/admin-settings/apps/carbone/settings');
@@ -60,7 +57,7 @@ test.describe('Admin Applications', () => {
adminApplicationsPage,
adminApplicationSettingsPage,
flowEditorPage,
page,
page
}) => {
await adminApplicationsPage.openApplication('Spotify');
await expect(page.url()).toContain('/admin-settings/apps/spotify/settings');
@@ -78,15 +75,11 @@ test.describe('Admin Applications', () => {
const triggerStep = flowEditorPage.flowStep.last();
await triggerStep.click();
await flowEditorPage.chooseAppAndEvent('Spotify', 'Create Playlist');
await flowEditorPage.chooseAppAndEvent("Spotify", "Create Playlist");
await flowEditorPage.connectionAutocomplete.click();
const newConnectionOption = page
.getByRole('option')
.filter({ hasText: 'Add new connection' });
const newSharedConnectionOption = page
.getByRole('option')
.filter({ hasText: 'Add new shared connection' });
const newConnectionOption = page.getByRole('option').filter({ hasText: 'Add new connection' });
const newSharedConnectionOption = page.getByRole('option').filter({ hasText: 'Add new shared connection' });
await expect(newConnectionOption).toBeEnabled();
await expect(newConnectionOption).toHaveCount(1);
@@ -98,7 +91,7 @@ test.describe('Admin Applications', () => {
adminApplicationSettingsPage,
adminApplicationAuthClientsPage,
flowEditorPage,
page,
page
}) => {
await adminApplicationsPage.openApplication('Reddit');
await expect(page.url()).toContain('/admin-settings/apps/reddit/settings');
@@ -108,21 +101,13 @@ test.describe('Admin Applications', () => {
await adminApplicationAuthClientsPage.openAuthClientsTab();
await adminApplicationAuthClientsPage.openFirstAuthClientCreateForm();
const authClientForm = page.getByTestId('auth-client-form');
const authClientForm = page.getByTestId("auth-client-form");
await authClientForm.locator(page.getByTestId('switch')).check();
await authClientForm
.locator(page.locator('[name="name"]'))
.fill('redditAuthClient');
await authClientForm
.locator(page.locator('[name="clientId"]'))
.fill('redditClientId');
await authClientForm
.locator(page.locator('[name="clientSecret"]'))
.fill('redditClientSecret');
await authClientForm.locator(page.locator('[name="name"]')).fill('redditAuthClient');
await authClientForm.locator(page.locator('[name="clientId"]')).fill('redditClientId');
await authClientForm.locator(page.locator('[name="clientSecret"]')).fill('redditClientSecret');
await adminApplicationAuthClientsPage.submitAuthClientForm();
await adminApplicationAuthClientsPage.authClientShouldBeVisible(
'redditAuthClient'
);
await adminApplicationAuthClientsPage.authClientShouldBeVisible('redditAuthClient');
await page.goto('/');
await page.getByTestId('create-flow-button').click();
@@ -134,15 +119,11 @@ test.describe('Admin Applications', () => {
const triggerStep = flowEditorPage.flowStep.last();
await triggerStep.click();
await flowEditorPage.chooseAppAndEvent('Reddit', 'Create link post');
await flowEditorPage.chooseAppAndEvent("Reddit", "Create link post");
await flowEditorPage.connectionAutocomplete.click();
const newConnectionOption = page
.getByRole('option')
.filter({ hasText: 'Add new connection' });
const newSharedConnectionOption = page
.getByRole('option')
.filter({ hasText: 'Add new shared connection' });
const newConnectionOption = page.getByRole('option').filter({ hasText: 'Add new connection' });
const newSharedConnectionOption = page.getByRole('option').filter({ hasText: 'Add new shared connection' });
await expect(newConnectionOption).toHaveCount(0);
await expect(newSharedConnectionOption).toBeEnabled();
@@ -153,7 +134,7 @@ test.describe('Admin Applications', () => {
adminApplicationsPage,
adminApplicationSettingsPage,
flowEditorPage,
page,
page
}) => {
await adminApplicationsPage.openApplication('DeepL');
await expect(page.url()).toContain('/admin-settings/apps/deepl/settings');
@@ -171,18 +152,12 @@ test.describe('Admin Applications', () => {
const triggerStep = flowEditorPage.flowStep.last();
await triggerStep.click();
await flowEditorPage.chooseAppAndEvent('DeepL', 'Translate text');
await flowEditorPage.chooseAppAndEvent("DeepL", "Translate text");
await flowEditorPage.connectionAutocomplete.click();
const newConnectionOption = page
.getByRole('option')
.filter({ hasText: 'Add new connection' });
const newSharedConnectionOption = page
.getByRole('option')
.filter({ hasText: 'Add new shared connection' });
const noConnectionsOption = page
.locator('.MuiAutocomplete-noOptions')
.filter({ hasText: 'No options' });
const newConnectionOption = page.getByRole('option').filter({ hasText: 'Add new connection' });
const newSharedConnectionOption = page.getByRole('option').filter({ hasText: 'Add new shared connection' });
const noConnectionsOption = page.locator('.MuiAutocomplete-noOptions').filter({ hasText: 'No options' });
await expect(noConnectionsOption).toHaveCount(1);
await expect(newConnectionOption).toHaveCount(0);
@@ -193,11 +168,11 @@ test.describe('Admin Applications', () => {
adminApplicationsPage,
adminApplicationSettingsPage,
flowEditorPage,
page,
page
}) => {
const queryUser = {
text: 'SELECT * FROM users WHERE email = $1',
values: [process.env.LOGIN_EMAIL],
values: [process.env.LOGIN_EMAIL]
};
try {
@@ -208,16 +183,14 @@ test.describe('Admin Applications', () => {
text: 'INSERT INTO connections (key, data, user_id, verified, draft) VALUES ($1, $2, $3, $4, $5)',
values: [
'mailchimp',
'U2FsdGVkX1+cAtdHwLiuRL4DaK/T1aljeeKyPMmtWK0AmAIsKhYwQiuyQCYJO3mdZ31z73hqF2Y+yj2Kn2/IIpLRqCxB2sC0rCDCZyolzOZ290YcBXSzYRzRUxhoOcZEtwYDKsy8AHygKK/tkj9uv9k6wOe1LjipNik4VmRhKjEYizzjLrJpbeU1oY+qW0GBpPYomFTeNf+MejSSmsUYyYJ8+E/4GeEfaonvsTSwMT7AId98Lck6Vy4wrfgpm7sZZ8xU15/HqXZNc8UCo2iTdw45xj/Oov9+brX4WUASFPG8aYrK8dl/EdaOvr89P8uIofbSNZ25GjJvVF5ymarrPkTZ7djjJXchzpwBY+7GTJfs3funR/vIk0Hq95jgOFFP1liZyqTXSa49ojG3hzojRQ==',
"U2FsdGVkX1+cAtdHwLiuRL4DaK/T1aljeeKyPMmtWK0AmAIsKhYwQiuyQCYJO3mdZ31z73hqF2Y+yj2Kn2/IIpLRqCxB2sC0rCDCZyolzOZ290YcBXSzYRzRUxhoOcZEtwYDKsy8AHygKK/tkj9uv9k6wOe1LjipNik4VmRhKjEYizzjLrJpbeU1oY+qW0GBpPYomFTeNf+MejSSmsUYyYJ8+E/4GeEfaonvsTSwMT7AId98Lck6Vy4wrfgpm7sZZ8xU15/HqXZNc8UCo2iTdw45xj/Oov9+brX4WUASFPG8aYrK8dl/EdaOvr89P8uIofbSNZ25GjJvVF5ymarrPkTZ7djjJXchzpwBY+7GTJfs3funR/vIk0Hq95jgOFFP1liZyqTXSa49ojG3hzojRQ==",
queryUserResult.rows[0].id,
'true',
'false',
'false'
],
};
const createMailchimpConnectionResult = await pgPool.query(
createMailchimpConnection
);
const createMailchimpConnectionResult = await pgPool.query(createMailchimpConnection);
expect(createMailchimpConnectionResult.rowCount).toBe(1);
expect(createMailchimpConnectionResult.command).toBe('INSERT');
} catch (err) {
@@ -226,9 +199,7 @@ test.describe('Admin Applications', () => {
}
await adminApplicationsPage.openApplication('Mailchimp');
await expect(page.url()).toContain(
'/admin-settings/apps/mailchimp/settings'
);
await expect(page.url()).toContain('/admin-settings/apps/mailchimp/settings');
await adminApplicationSettingsPage.disallowConnections();
await adminApplicationSettingsPage.expectSuccessSnackbarToBeVisible();
@@ -243,22 +214,14 @@ test.describe('Admin Applications', () => {
const triggerStep = flowEditorPage.flowStep.last();
await triggerStep.click();
await flowEditorPage.chooseAppAndEvent('Mailchimp', 'Create campaign');
await flowEditorPage.chooseAppAndEvent("Mailchimp", "Create campaign");
await flowEditorPage.connectionAutocomplete.click();
await expect(page.getByRole('option').first()).toHaveText('Unnamed');
const existingConnection = page
.getByRole('option')
.filter({ hasText: 'Unnamed' });
const newConnectionOption = page
.getByRole('option')
.filter({ hasText: 'Add new connection' });
const newSharedConnectionOption = page
.getByRole('option')
.filter({ hasText: 'Add new shared connection' });
const noConnectionsOption = page
.locator('.MuiAutocomplete-noOptions')
.filter({ hasText: 'No options' });
const existingConnection = page.getByRole('option').filter({ hasText: 'Unnamed' });
const newConnectionOption = page.getByRole('option').filter({ hasText: 'Add new connection' });
const newSharedConnectionOption = page.getByRole('option').filter({ hasText: 'Add new shared connection' });
const noConnectionsOption = page.locator('.MuiAutocomplete-noOptions').filter({ hasText: 'No options' });
await expect(await existingConnection.count()).toBeGreaterThan(0);
await expect(noConnectionsOption).toHaveCount(0);

View File

@@ -22,18 +22,22 @@ test.describe('Role management page', () => {
await adminRolesPage.navigateTo();
await adminRolesPage.createRoleButton.click();
await adminCreateRolePage.isMounted();
await adminCreateRolePage.waitForPermissionsCatalogToVisible();
await adminCreateRolePage.nameInput.fill('Create Edit Test');
await adminCreateRolePage.descriptionInput.fill('Test description');
await adminCreateRolePage.createButton.click();
await adminCreateRolePage.snackbar.waitFor({
state: 'attached',
});
const snackbar = await adminCreateRolePage.getSnackbarData(
'snackbar-create-role-success'
);
await expect(snackbar.variant).toBe('success');
await adminCreateRolePage.closeSnackbar();
});
let roleRow =
await test.step('Make sure role data is correct', async () => {
let roleRow = await test.step(
'Make sure role data is correct',
async () => {
const roleRow = await adminRolesPage.getRoleRowByName(
'Create Edit Test'
);
@@ -44,7 +48,8 @@ test.describe('Role management page', () => {
await expect(roleData.canEdit).toBe(true);
await expect(roleData.canDelete).toBe(true);
return roleRow;
});
}
);
await test.step('Edit the role', async () => {
await adminRolesPage.clickEditRole(roleRow);
@@ -52,14 +57,19 @@ test.describe('Role management page', () => {
await adminEditRolePage.nameInput.fill('Create Update Test');
await adminEditRolePage.descriptionInput.fill('Update test description');
await adminEditRolePage.updateButton.click();
await adminEditRolePage.snackbar.waitFor({
state: 'attached',
});
const snackbar = await adminEditRolePage.getSnackbarData(
'snackbar-edit-role-success'
);
await expect(snackbar.variant).toBe('success');
await adminEditRolePage.closeSnackbar();
});
roleRow =
await test.step('Make sure changes reflected on roles page', async () => {
roleRow = await test.step(
'Make sure changes reflected on roles page',
async () => {
await adminRolesPage.isMounted();
const roleRow = await adminRolesPage.getRoleRowByName(
'Create Update Test'
@@ -71,7 +81,8 @@ test.describe('Role management page', () => {
await expect(roleData.canEdit).toBe(true);
await expect(roleData.canDelete).toBe(true);
return roleRow;
});
}
);
await test.step('Delete the role', async () => {
await adminRolesPage.clickDeleteRole(roleRow);
@@ -80,10 +91,14 @@ test.describe('Role management page', () => {
state: 'attached',
});
await deleteModal.deleteButton.click();
await adminRolesPage.snackbar.waitFor({
state: 'attached',
});
const snackbar = await adminRolesPage.getSnackbarData(
'snackbar-delete-role-success'
);
await expect(snackbar.variant).toBe('success');
await adminRolesPage.closeSnackbar();
await deleteModal.modal.waitFor({
state: 'detached',
});
@@ -158,45 +173,60 @@ test.describe('Role management page', () => {
await test.step('Create a new role', async () => {
await adminRolesPage.createRoleButton.click();
await adminCreateRolePage.isMounted();
await adminCreateRolePage.waitForPermissionsCatalogToVisible();
await adminCreateRolePage.nameInput.fill('Delete Role');
await adminCreateRolePage.createButton.click();
await adminCreateRolePage.snackbar.waitFor({
state: 'attached',
});
const snackbar = await adminCreateRolePage.getSnackbarData(
'snackbar-create-role-success'
);
await expect(snackbar.variant).toBe('success');
await adminCreateRolePage.closeSnackbar();
});
await test.step('Create a new user with the "Delete Role" role', async () => {
await adminUsersPage.navigateTo();
await adminUsersPage.createUserButton.click();
await adminCreateUserPage.fullNameInput.fill('User Role Test');
await adminCreateUserPage.emailInput.fill(
'user-role-test@automatisch.io'
);
await adminCreateUserPage.roleInput.click();
await adminCreateUserPage.page
.getByRole('option', { name: 'Delete Role', exact: true })
.click();
await adminCreateUserPage.createButton.click();
await adminCreateUserPage.invitationEmailInfoAlert.waitFor({
state: 'attached',
});
const snackbar = await adminUsersPage.getSnackbarData(
'snackbar-create-user-success'
);
await expect(snackbar.variant).toBe('success');
});
await test.step('Try to delete "Delete Role" role when new user has it', async () => {
await adminRolesPage.navigateTo();
const row = await adminRolesPage.getRoleRowByName('Delete Role');
const modal = await adminRolesPage.clickDeleteRole(row);
await modal.deleteButton.click();
const snackbar = await adminRolesPage.getSnackbarData(
'snackbar-delete-role-error'
);
await expect(snackbar.variant).toBe('error');
await modal.close();
});
await test.step(
'Create a new user with the "Delete Role" role',
async () => {
await adminUsersPage.navigateTo();
await adminUsersPage.createUserButton.click();
await adminCreateUserPage.fullNameInput.fill('User Role Test');
await adminCreateUserPage.emailInput.fill(
'user-role-test@automatisch.io'
);
await adminCreateUserPage.roleInput.click();
await adminCreateUserPage.page
.getByRole('option', { name: 'Delete Role', exact: true })
.click();
await adminCreateUserPage.createButton.click();
await adminCreateUserPage.snackbar.waitFor({
state: 'attached',
});
await adminCreateUserPage.invitationEmailInfoAlert.waitFor({
state: 'attached',
});
const snackbar = await adminUsersPage.getSnackbarData(
'snackbar-create-user-success'
);
await expect(snackbar.variant).toBe('success');
await adminUsersPage.closeSnackbar();
}
);
await test.step(
'Try to delete "Delete Role" role when new user has it',
async () => {
await adminRolesPage.navigateTo();
const row = await adminRolesPage.getRoleRowByName('Delete Role');
const modal = await adminRolesPage.clickDeleteRole(row);
await modal.deleteButton.click();
await adminRolesPage.snackbar.waitFor({
state: 'attached',
});
const snackbar = await adminRolesPage.getSnackbarData('snackbar-delete-role-error');
await expect(snackbar.variant).toBe('error');
await adminRolesPage.closeSnackbar();
await modal.close();
}
);
await test.step('Change the role the user has', async () => {
await adminUsersPage.navigateTo();
await adminUsersPage.usersLoader.waitFor({
@@ -211,10 +241,14 @@ test.describe('Role management page', () => {
.getByRole('option', { name: 'Admin' })
.click();
await adminEditUserPage.updateButton.click();
await adminEditUserPage.snackbar.waitFor({
state: 'attached',
});
const snackbar = await adminEditUserPage.getSnackbarData(
'snackbar-edit-user-success'
);
await expect(snackbar.variant).toBe('success');
await adminEditUserPage.closeSnackbar();
});
await test.step('Delete the original role', async () => {
await adminRolesPage.navigateTo();
@@ -222,10 +256,14 @@ test.describe('Role management page', () => {
const modal = await adminRolesPage.clickDeleteRole(row);
await expect(modal.modal).toBeVisible();
await modal.deleteButton.click();
await adminRolesPage.snackbar.waitFor({
state: 'attached',
});
const snackbar = await adminRolesPage.getSnackbarData(
'snackbar-delete-role-success'
);
await expect(snackbar.variant).toBe('success');
await adminRolesPage.closeSnackbar();
});
});
@@ -239,13 +277,16 @@ test.describe('Role management page', () => {
await test.step('Create a new role', async () => {
await adminRolesPage.createRoleButton.click();
await adminCreateRolePage.isMounted();
await adminCreateRolePage.waitForPermissionsCatalogToVisible();
await adminCreateRolePage.nameInput.fill('Cannot Delete Role');
await adminCreateRolePage.createButton.click();
await adminCreateRolePage.snackbar.waitFor({
state: 'attached',
});
const snackbar = await adminCreateRolePage.getSnackbarData(
'snackbar-create-role-success'
);
await expect(snackbar.variant).toBe('success');
await adminCreateRolePage.closeSnackbar();
});
await test.step('Create a new user with this role', async () => {
await adminUsersPage.navigateTo();
@@ -260,6 +301,9 @@ test.describe('Role management page', () => {
.getByRole('option', { name: 'Cannot Delete Role' })
.click();
await adminCreateUserPage.createButton.click();
await adminCreateUserPage.snackbar.waitFor({
state: 'attached',
});
await adminCreateUserPage.invitationEmailInfoAlert.waitFor({
state: 'attached',
});
@@ -267,34 +311,40 @@ test.describe('Role management page', () => {
'snackbar-create-user-success'
);
await expect(snackbar.variant).toBe('success');
await adminCreateUserPage.closeSnackbar();
});
await test.step('Delete this user', async () => {
await adminUsersPage.navigateTo();
const row = await adminUsersPage.findUserPageWithEmail(
'user-delete-role-test@automatisch.io'
);
// await test.waitForTimeout(10000);
const modal = await adminUsersPage.clickDeleteUser(row);
await modal.deleteButton.click();
await adminUsersPage.snackbar.waitFor({
state: 'attached',
});
const snackbar = await adminUsersPage.getSnackbarData(
'snackbar-delete-user-success'
);
await expect(snackbar.variant).toBe('success');
await adminUsersPage.closeSnackbar();
});
await test.step('Try deleting this role', async () => {
await adminRolesPage.navigateTo();
const row = await adminRolesPage.getRoleRowByName('Cannot Delete Role');
const modal = await adminRolesPage.clickDeleteRole(row);
await modal.deleteButton.click();
const snackbar = await adminRolesPage.getSnackbarData(
'snackbar-delete-role-error'
);
await expect(snackbar.variant).toBe('error');
await adminRolesPage.snackbar.waitFor({
state: 'attached',
});
/*
* TODO: await snackbar - make assertions based on product
* decisions
const snackbar = await adminRolesPage.getSnackbarData();
await expect(snackbar.variant).toBe('...');
*/
await adminRolesPage.closeSnackbar();
});
});
});
@@ -312,13 +362,16 @@ test('Accessibility of role management page', async ({
await adminRolesPage.navigateTo();
await adminRolesPage.createRoleButton.click();
await adminCreateRolePage.isMounted();
await adminCreateRolePage.waitForPermissionsCatalogToVisible();
await adminCreateRolePage.nameInput.fill('Basic Test');
await adminCreateRolePage.createButton.click();
await adminCreateRolePage.snackbar.waitFor({
state: 'attached',
});
const snackbar = await adminCreateRolePage.getSnackbarData(
'snackbar-create-role-success'
);
await expect(snackbar.variant).toBe('success');
await adminCreateRolePage.closeSnackbar();
});
await test.step('Create a new user with the basic role', async () => {
@@ -332,6 +385,9 @@ test('Accessibility of role management page', async ({
.getByRole('option', { name: 'Basic Test' })
.click();
await adminCreateUserPage.createButton.click();
await adminCreateUserPage.snackbar.waitFor({
state: 'attached',
});
await adminCreateUserPage.invitationEmailInfoAlert.waitFor({
state: 'attached',
});
@@ -339,46 +395,56 @@ test('Accessibility of role management page', async ({
'snackbar-create-user-success'
);
await expect(snackbar.variant).toBe('success');
await adminCreateUserPage.closeSnackbar();
});
await test.step('Logout and login to the basic role user', async () => {
const acceptInvitationLink = await adminCreateUserPage.acceptInvitationLink;
console.log(acceptInvitationLink);
const acceptInvitationUrl = await acceptInvitationLink.textContent();
console.log(acceptInvitationUrl);
const acceptInvitatonToken = acceptInvitationUrl.split('?token=')[1];
await page.getByTestId('profile-menu-button').click();
await page.getByTestId('logout-item').click();
const acceptInvitationPage = new AcceptInvitation(page);
await acceptInvitationPage.open(acceptInvitatonToken);
await acceptInvitationPage.acceptInvitation('sample');
const loginPage = new LoginPage(page);
// await loginPage.isMounted();
await loginPage.login('basic-role-test@automatisch.io', 'sample');
await expect(loginPage.loginButton).not.toBeVisible();
await expect(page).toHaveURL('/flows');
});
await test.step('Navigate to the admin settings page and make sure it is blank', async () => {
const pageUrl = new URL(page.url());
const url = `${pageUrl.origin}/admin-settings/users`;
await page.goto(url);
await page.waitForTimeout(750);
const isUnmounted = await page.evaluate(() => {
// eslint-disable-next-line no-undef
const root = document.querySelector('#root');
await test.step(
'Navigate to the admin settings page and make sure it is blank',
async () => {
const pageUrl = new URL(page.url());
const url = `${pageUrl.origin}/admin-settings/users`;
await page.goto(url);
await page.waitForTimeout(750);
const isUnmounted = await page.evaluate(() => {
// eslint-disable-next-line no-undef
const root = document.querySelector('#root');
if (root) {
// We have react query devtools only in dev env.
// In production, there is nothing in root.
// That's why `<= 1`.
return root.children.length <= 1;
}
if (root) {
// We have react query devtools only in dev env.
// In production, there is nothing in root.
// That's why `<= 1`.
return root.children.length <= 1;
}
return false;
});
await expect(isUnmounted).toBe(true);
});
return false;
});
await expect(isUnmounted).toBe(true);
}
);
await test.step('Log back into the admin account', async () => {
await page.goto('/');
@@ -399,10 +465,10 @@ test('Accessibility of role management page', async ({
await adminEditUserPage.roleInput.click();
await adminEditUserPage.page.getByRole('option', { name: 'Admin' }).click();
await adminEditUserPage.updateButton.click();
const snackbar = await adminEditUserPage.getSnackbarData(
'snackbar-edit-user-success'
);
await expect(snackbar.variant).toBe('success');
await adminEditUserPage.snackbar.waitFor({
state: 'attached',
});
await adminEditUserPage.closeSnackbar();
});
await test.step('Delete the role', async () => {
@@ -414,10 +480,14 @@ test('Accessibility of role management page', async ({
state: 'attached',
});
await deleteModal.deleteButton.click();
await adminRolesPage.snackbar.waitFor({
state: 'attached',
});
const snackbar = await adminRolesPage.getSnackbarData(
'snackbar-delete-role-success'
);
await expect(snackbar.variant).toBe('success');
await adminRolesPage.closeSnackbar();
await deleteModal.modal.waitFor({
state: 'detached',
});

View File

@@ -5,235 +5,281 @@ const { test, expect } = require('../../fixtures/index');
* otherwise tests will fail since users are only *soft*-deleted
*/
test.describe('User management page', () => {
test.beforeEach(async ({ adminUsersPage }) => {
await adminUsersPage.navigateTo();
await adminUsersPage.closeAllSnackbars();
await adminUsersPage.closeSnackbar();
});
test('User creation and deletion process', async ({
adminCreateUserPage,
adminEditUserPage,
adminUsersPage,
}) => {
adminCreateUserPage.seed(9000);
const user = adminCreateUserPage.generateUser();
await adminUsersPage.usersLoader.waitFor({
state: 'detached' /* Note: state: 'visible' introduces flakiness
test(
'User creation and deletion process',
async ({ adminCreateUserPage, adminEditUserPage, adminUsersPage }) => {
adminCreateUserPage.seed(9000);
const user = adminCreateUserPage.generateUser();
await adminUsersPage.usersLoader.waitFor({
state: 'detached' /* Note: state: 'visible' introduces flakiness
because visibility: hidden is used as part of the state transition in
notistack, see
https://github.com/iamhosseindhv/notistack/blob/122f47057eb7ce5a1abfe923316cf8475303e99a/src/transitions/Collapse/Collapse.tsx#L110
*/,
});
await test.step('Create a user', async () => {
await adminUsersPage.createUserButton.click();
await adminCreateUserPage.fullNameInput.fill(user.fullName);
await adminCreateUserPage.emailInput.fill(user.email);
await adminCreateUserPage.roleInput.click();
await adminCreateUserPage.page
.getByRole('option', { name: 'Admin' })
.click();
await adminCreateUserPage.createButton.click();
await adminCreateUserPage.invitationEmailInfoAlert.waitFor({
state: 'attached',
*/
});
await test.step(
'Create a user',
async () => {
await adminUsersPage.createUserButton.click();
await adminCreateUserPage.fullNameInput.fill(user.fullName);
await adminCreateUserPage.emailInput.fill(user.email);
await adminCreateUserPage.roleInput.click();
await adminCreateUserPage.page.getByRole(
'option', { name: 'Admin' }
).click();
await adminCreateUserPage.createButton.click();
await adminCreateUserPage.invitationEmailInfoAlert.waitFor({
state: 'attached'
});
const snackbar = await adminUsersPage.getSnackbarData(
'snackbar-create-user-success'
const snackbar = await adminUsersPage.getSnackbarData(
'snackbar-create-user-success'
);
await expect(snackbar.variant).toBe('success');
await adminUsersPage.navigateTo();
await adminUsersPage.closeSnackbar();
}
);
await expect(snackbar.variant).toBe('success');
await adminUsersPage.navigateTo();
});
await test.step('Check the user exists with the expected properties', async () => {
await adminUsersPage.findUserPageWithEmail(user.email);
const userRow = await adminUsersPage.getUserRowByEmail(user.email);
const data = await adminUsersPage.getRowData(userRow);
await expect(data.email).toBe(user.email);
await expect(data.fullName).toBe(user.fullName);
await expect(data.role).toBe('Admin');
});
await test.step('Edit user info and make sure the edit works correctly', async () => {
await adminUsersPage.findUserPageWithEmail(user.email);
let userRow = await adminUsersPage.getUserRowByEmail(user.email);
await adminUsersPage.clickEditUser(userRow);
await adminEditUserPage.waitForLoad(user.fullName);
const newUserInfo = adminEditUserPage.generateUser();
await adminEditUserPage.fullNameInput.fill(newUserInfo.fullName);
await adminEditUserPage.updateButton.click();
const snackbar = await adminUsersPage.getSnackbarData(
'snackbar-edit-user-success'
await test.step(
'Check the user exists with the expected properties',
async () => {
await adminUsersPage.findUserPageWithEmail(user.email);
const userRow = await adminUsersPage.getUserRowByEmail(user.email);
const data = await adminUsersPage.getRowData(userRow);
await expect(data.email).toBe(user.email);
await expect(data.fullName).toBe(user.fullName);
await expect(data.role).toBe('Admin');
}
);
await expect(snackbar.variant).toBe('success');
await test.step(
'Edit user info and make sure the edit works correctly',
async () => {
await adminUsersPage.findUserPageWithEmail(user.email);
await adminUsersPage.findUserPageWithEmail(user.email);
userRow = await adminUsersPage.getUserRowByEmail(user.email);
const rowData = await adminUsersPage.getRowData(userRow);
await expect(rowData.fullName).toBe(newUserInfo.fullName);
});
await test.step('Delete user and check the page confirms this deletion', async () => {
await adminUsersPage.findUserPageWithEmail(user.email);
const userRow = await adminUsersPage.getUserRowByEmail(user.email);
await adminUsersPage.clickDeleteUser(userRow);
const modal = adminUsersPage.deleteUserModal;
await modal.deleteButton.click();
let userRow = await adminUsersPage.getUserRowByEmail(user.email);
await adminUsersPage.clickEditUser(userRow);
await adminEditUserPage.waitForLoad(user.fullName);
const newUserInfo = adminEditUserPage.generateUser();
await adminEditUserPage.fullNameInput.fill(newUserInfo.fullName);
await adminEditUserPage.updateButton.click();
const snackbar = await adminUsersPage.getSnackbarData(
'snackbar-delete-user-success'
const snackbar = await adminUsersPage.getSnackbarData(
'snackbar-edit-user-success'
);
await expect(snackbar.variant).toBe('success');
await adminUsersPage.closeSnackbar();
await adminUsersPage.findUserPageWithEmail(user.email);
userRow = await adminUsersPage.getUserRowByEmail(user.email);
const rowData = await adminUsersPage.getRowData(userRow);
await expect(rowData.fullName).toBe(newUserInfo.fullName);
}
);
await expect(snackbar.variant).toBe('success');
await expect(userRow).not.toBeVisible(false);
});
});
await test.step(
'Delete user and check the page confirms this deletion',
async () => {
await adminUsersPage.findUserPageWithEmail(user.email);
const userRow = await adminUsersPage.getUserRowByEmail(user.email);
await adminUsersPage.clickDeleteUser(userRow);
const modal = adminUsersPage.deleteUserModal;
await modal.deleteButton.click();
test('Creating a user which has been deleted', async ({
adminCreateUserPage,
adminUsersPage,
}) => {
adminCreateUserPage.seed(9100);
const testUser = adminCreateUserPage.generateUser();
await test.step('Create the test user', async () => {
await adminUsersPage.navigateTo();
await adminUsersPage.createUserButton.click();
await adminCreateUserPage.fullNameInput.fill(testUser.fullName);
await adminCreateUserPage.emailInput.fill(testUser.email);
await adminCreateUserPage.roleInput.click();
await adminCreateUserPage.page
.getByRole('option', { name: 'Admin' })
.click();
await adminCreateUserPage.createButton.click();
const snackbar = await adminUsersPage.getSnackbarData(
'snackbar-create-user-success'
const snackbar = await adminUsersPage.getSnackbarData(
'snackbar-delete-user-success'
);
await expect(snackbar.variant).toBe('success');
await adminUsersPage.closeSnackbar();
await expect(userRow).not.toBeVisible(false);
}
);
await expect(snackbar.variant).toBe('success');
});
await test.step('Delete the created user', async () => {
await adminUsersPage.navigateTo();
await adminUsersPage.findUserPageWithEmail(testUser.email);
const userRow = await adminUsersPage.getUserRowByEmail(testUser.email);
await adminUsersPage.clickDeleteUser(userRow);
const modal = adminUsersPage.deleteUserModal;
await modal.deleteButton.click();
const snackbar = await adminUsersPage.getSnackbarData(
'snackbar-delete-user-success'
test(
'Creating a user which has been deleted',
async ({ adminCreateUserPage, adminUsersPage }) => {
adminCreateUserPage.seed(9100);
const testUser = adminCreateUserPage.generateUser();
await test.step(
'Create the test user',
async () => {
await adminUsersPage.navigateTo();
await adminUsersPage.createUserButton.click();
await adminCreateUserPage.fullNameInput.fill(testUser.fullName);
await adminCreateUserPage.emailInput.fill(testUser.email);
await adminCreateUserPage.roleInput.click();
await adminCreateUserPage.page.getByRole(
'option', { name: 'Admin' }
).click();
await adminCreateUserPage.createButton.click();
const snackbar = await adminUsersPage.getSnackbarData(
'snackbar-create-user-success'
);
await expect(snackbar.variant).toBe('success');
await adminUsersPage.closeSnackbar();
}
);
await expect(snackbar).not.toBeNull();
await expect(snackbar.variant).toBe('success');
await expect(userRow).not.toBeVisible(false);
});
await test.step('Create the user again', async () => {
await adminUsersPage.createUserButton.click();
await adminCreateUserPage.fullNameInput.fill(testUser.fullName);
await adminCreateUserPage.emailInput.fill(testUser.email);
await adminCreateUserPage.roleInput.click();
await adminCreateUserPage.page
.getByRole('option', { name: 'Admin' })
.click();
await adminCreateUserPage.createButton.click();
const snackbar = await adminUsersPage.getSnackbarData('snackbar-error');
await expect(snackbar.variant).toBe('error');
});
});
test('Creating a user which already exists', async ({
adminCreateUserPage,
adminUsersPage,
page,
}) => {
adminCreateUserPage.seed(9200);
const testUser = adminCreateUserPage.generateUser();
await test.step('Create the test user', async () => {
await adminUsersPage.createUserButton.click();
await adminCreateUserPage.fullNameInput.fill(testUser.fullName);
await adminCreateUserPage.emailInput.fill(testUser.email);
await adminCreateUserPage.roleInput.click();
await adminCreateUserPage.page
.getByRole('option', { name: 'Admin' })
.click();
await adminCreateUserPage.createButton.click();
const snackbar = await adminUsersPage.getSnackbarData(
'snackbar-create-user-success'
await test.step(
'Delete the created user',
async () => {
await adminUsersPage.navigateTo();
await adminUsersPage.findUserPageWithEmail(testUser.email);
const userRow = await adminUsersPage.getUserRowByEmail(testUser.email);
await adminUsersPage.clickDeleteUser(userRow);
const modal = adminUsersPage.deleteUserModal;
await modal.deleteButton.click();
const snackbar = await adminUsersPage.getSnackbarData(
'snackbar-delete-user-success'
);
await expect(snackbar).not.toBeNull();
await expect(snackbar.variant).toBe('success');
await adminUsersPage.closeSnackbar();
await expect(userRow).not.toBeVisible(false);
}
);
await expect(snackbar.variant).toBe('success');
});
await test.step('Create the user again', async () => {
await adminUsersPage.navigateTo();
await adminUsersPage.createUserButton.click();
await adminCreateUserPage.fullNameInput.fill(testUser.fullName);
await adminCreateUserPage.emailInput.fill(testUser.email);
const createUserPageUrl = page.url();
await adminCreateUserPage.roleInput.click();
await adminCreateUserPage.page
.getByRole('option', { name: 'Admin' })
.click();
await adminCreateUserPage.createButton.click();
await expect(page.url()).toBe(createUserPageUrl);
const snackbar = await adminUsersPage.getSnackbarData('snackbar-error');
await expect(snackbar.variant).toBe('error');
});
});
test('Editing a user to have the same email as another user should not be allowed', async ({
adminCreateUserPage,
adminEditUserPage,
adminUsersPage,
page,
}) => {
adminCreateUserPage.seed(9300);
const user1 = adminCreateUserPage.generateUser();
const user2 = adminCreateUserPage.generateUser();
await test.step('Create the first user', async () => {
await adminUsersPage.navigateTo();
await adminUsersPage.createUserButton.click();
await adminCreateUserPage.fullNameInput.fill(user1.fullName);
await adminCreateUserPage.emailInput.fill(user1.email);
await adminCreateUserPage.roleInput.click();
await adminCreateUserPage.page
.getByRole('option', { name: 'Admin' })
.click();
await adminCreateUserPage.createButton.click();
const snackbar = await adminUsersPage.getSnackbarData(
'snackbar-create-user-success'
await test.step(
'Create the user again',
async () => {
await adminUsersPage.createUserButton.click();
await adminCreateUserPage.fullNameInput.fill(testUser.fullName);
await adminCreateUserPage.emailInput.fill(testUser.email);
await adminCreateUserPage.roleInput.click();
await adminCreateUserPage.page.getByRole(
'option', { name: 'Admin' }
).click();
await adminCreateUserPage.createButton.click();
const snackbar = await adminUsersPage.getSnackbarData('snackbar-error');
await expect(snackbar.variant).toBe('error');
await adminUsersPage.closeSnackbar();
}
);
await expect(snackbar.variant).toBe('success');
await adminUsersPage.closeAllSnackbars();
});
}
);
await test.step('Create the second user', async () => {
await adminUsersPage.navigateTo();
await adminUsersPage.createUserButton.click();
await adminCreateUserPage.fullNameInput.fill(user2.fullName);
await adminCreateUserPage.emailInput.fill(user2.email);
await adminCreateUserPage.roleInput.click();
await adminCreateUserPage.page
.getByRole('option', { name: 'Admin' })
.click();
await adminCreateUserPage.createButton.click();
const snackbar = await adminUsersPage.getSnackbarData(
'snackbar-create-user-success'
test(
'Creating a user which already exists',
async ({ adminCreateUserPage, adminUsersPage, page }) => {
adminCreateUserPage.seed(9200);
const testUser = adminCreateUserPage.generateUser();
await test.step(
'Create the test user',
async () => {
await adminUsersPage.createUserButton.click();
await adminCreateUserPage.fullNameInput.fill(testUser.fullName);
await adminCreateUserPage.emailInput.fill(testUser.email);
await adminCreateUserPage.roleInput.click();
await adminCreateUserPage.page.getByRole(
'option', { name: 'Admin' }
).click();
await adminCreateUserPage.createButton.click();
const snackbar = await adminUsersPage.getSnackbarData(
'snackbar-create-user-success'
);
await expect(snackbar.variant).toBe('success');
await adminUsersPage.closeSnackbar();
}
);
await expect(snackbar.variant).toBe('success');
});
await test.step('Try editing the second user to have the email of the first user', async () => {
await adminUsersPage.navigateTo();
await adminUsersPage.findUserPageWithEmail(user2.email);
let userRow = await adminUsersPage.getUserRowByEmail(user2.email);
await adminUsersPage.clickEditUser(userRow);
await adminEditUserPage.waitForLoad(user2.fullName);
await adminEditUserPage.emailInput.fill(user1.email);
const editPageUrl = page.url();
await adminEditUserPage.updateButton.click();
await test.step(
'Create the user again',
async () => {
await adminUsersPage.navigateTo();
await adminUsersPage.createUserButton.click();
await adminCreateUserPage.fullNameInput.fill(testUser.fullName);
await adminCreateUserPage.emailInput.fill(testUser.email);
const createUserPageUrl = page.url();
await adminCreateUserPage.roleInput.click();
await adminCreateUserPage.page.getByRole(
'option', { name: 'Admin' }
).click();
await adminCreateUserPage.createButton.click();
const snackbar = await adminUsersPage.getSnackbarData('snackbar-error');
await expect(snackbar.variant).toBe('error');
await expect(page.url()).toBe(editPageUrl);
});
});
await expect(page.url()).toBe(createUserPageUrl);
const snackbar = await adminUsersPage.getSnackbarData('snackbar-error');
await expect(snackbar.variant).toBe('error');
await adminUsersPage.closeSnackbar();
}
);
}
);
test(
'Editing a user to have the same email as another user should not be allowed',
async ({
adminCreateUserPage, adminEditUserPage, adminUsersPage, page
}) => {
adminCreateUserPage.seed(9300);
const user1 = adminCreateUserPage.generateUser();
const user2 = adminCreateUserPage.generateUser();
await test.step(
'Create the first user',
async () => {
await adminUsersPage.navigateTo();
await adminUsersPage.createUserButton.click();
await adminCreateUserPage.fullNameInput.fill(user1.fullName);
await adminCreateUserPage.emailInput.fill(user1.email);
await adminCreateUserPage.roleInput.click();
await adminCreateUserPage.page.getByRole(
'option', { name: 'Admin' }
).click();
await adminCreateUserPage.createButton.click();
const snackbar = await adminUsersPage.getSnackbarData(
'snackbar-create-user-success'
);
await expect(snackbar.variant).toBe('success');
await adminUsersPage.closeSnackbar();
}
);
await test.step(
'Create the second user',
async () => {
await adminUsersPage.navigateTo();
await adminUsersPage.createUserButton.click();
await adminCreateUserPage.fullNameInput.fill(user2.fullName);
await adminCreateUserPage.emailInput.fill(user2.email);
await adminCreateUserPage.roleInput.click();
await adminCreateUserPage.page.getByRole(
'option', { name: 'Admin' }
).click();
await adminCreateUserPage.createButton.click();
const snackbar = await adminUsersPage.getSnackbarData(
'snackbar-create-user-success'
);
await expect(snackbar.variant).toBe('success');
await adminUsersPage.closeSnackbar();
}
);
await test.step(
'Try editing the second user to have the email of the first user',
async () => {
await adminUsersPage.navigateTo();
await adminUsersPage.findUserPageWithEmail(user2.email);
let userRow = await adminUsersPage.getUserRowByEmail(user2.email);
await adminUsersPage.clickEditUser(userRow);
await adminEditUserPage.waitForLoad(user2.fullName);
await adminEditUserPage.emailInput.fill(user1.email);
const editPageUrl = page.url();
await adminEditUserPage.updateButton.click();
const snackbar = await adminUsersPage.getSnackbarData(
'snackbar-error'
);
await expect(snackbar.variant).toBe('error');
await adminUsersPage.closeSnackbar();
await expect(page.url()).toBe(editPageUrl);
}
);
}
);
});

View File

@@ -1,55 +1,24 @@
const { request } = require('@playwright/test');
const { test, expect } = require('../../fixtures/index');
const {
AddMattermostConnectionModal,
} = require('../../fixtures/apps/mattermost/add-mattermost-connection-modal');
const { createFlow, updateFlowName, getFlow, updateFlowStep, testStep } = require('../../helpers/flow-api-helper');
const { getToken } = require('../../helpers/auth-api-helper');
const {AddMattermostConnectionModal} = require('../../fixtures/apps/mattermost/add-mattermost-connection-modal');
test.describe('Pop-up message on connections', () => {
test.beforeEach(async ({ flowEditorPage, page }) => {
const apiRequest = await request.newContext();
const tokenJsonResponse = await getToken(apiRequest);
const token = tokenJsonResponse.data.token;
await page.getByTestId('create-flow-button').click();
await page.waitForURL(
/\/editor\/[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}/
);
await expect(page.getByTestId('flow-step')).toHaveCount(2);
let flow = await createFlow(apiRequest, token);
const flowId = flow.data.id;
await updateFlowName(apiRequest, token, flowId);
flow = await getFlow(apiRequest, token, flowId);
const flowSteps = flow.data.steps;
await flowEditorPage.flowName.click();
await flowEditorPage.flowNameInput.fill('PopupFlow');
await flowEditorPage.createWebhookTrigger(true);
const triggerStepId = flowSteps.find((step) => step.type === 'trigger').id;
const actionStepId = flowSteps.find((step) => step.type === 'action').id;
const triggerStep = await updateFlowStep(apiRequest, token, triggerStepId, {
appKey: 'webhook',
key: 'catchRawWebhook',
parameters: {
workSynchronously: false,
},
});
await apiRequest.get(triggerStep.data.webhookUrl);
await testStep(apiRequest, token, triggerStepId);
await updateFlowStep(apiRequest, token, actionStepId, {
appKey: 'mattermost',
key: 'sendMessageToChannel',
});
await testStep(apiRequest, token, actionStepId);
await page.reload();
const flowRow = await page.getByTestId('flow-row').filter({
hasText: flowId,
});
await flowRow.click();
const flowTriggerStep = await page.getByTestId('flow-step').nth(1);
await flowTriggerStep.click();
await page.getByText('Choose connection').click();
await flowEditorPage.chooseAppAndEvent('Mattermost', 'Send a message to channel');
await expect(flowEditorPage.continueButton).toHaveCount(1);
await expect(flowEditorPage.continueButton).not.toBeEnabled();
await flowEditorPage.connectionAutocomplete.click();
await flowEditorPage.addNewConnectionItem.click();
});
await flowEditorPage.addNewConnectionItem.click(); });
test('should show error to remind to enable pop-up on connection create', async ({
page,
@@ -59,7 +28,7 @@ test.describe('Pop-up message on connections', () => {
// Inject script to override window.open
await page.evaluate(() => {
// eslint-disable-next-line no-undef
window.open = function () {
window.open = function() {
console.log('Popup blocked!');
return null;
};
@@ -68,10 +37,8 @@ test.describe('Pop-up message on connections', () => {
await addMattermostConnectionModal.fillConnectionForm();
await addMattermostConnectionModal.submitConnectionForm();
await expect(page.getByTestId('add-connection-error')).toHaveCount(1);
await expect(page.getByTestId('add-connection-error')).toHaveText(
'Make sure pop-ups are enabled in your browser.'
);
await expect(page.getByTestId("add-connection-error")).toHaveCount(1);
await expect(page.getByTestId("add-connection-error")).toHaveText('Make sure pop-ups are enabled in your browser.');
});
test('should not show pop-up error if pop-ups are enabled on connection create', async ({
@@ -84,15 +51,13 @@ test.describe('Pop-up message on connections', () => {
await addMattermostConnectionModal.submitConnectionForm();
const popup = await popupPromise;
await expect(popup.url()).toContain('mattermost');
await expect(page.getByTestId('add-connection-error')).toHaveCount(0);
await expect(popup.url()).toContain("mattermost");
await expect(page.getByTestId("add-connection-error")).toHaveCount(0);
await test.step('Should show error on failed credentials verification', async () => {
await popup.close();
await expect(page.getByTestId('add-connection-error')).toHaveCount(1);
await expect(page.getByTestId('add-connection-error')).toHaveText(
'Error occured while verifying credentials!'
);
await expect(page.getByTestId("add-connection-error")).toHaveCount(1);
await expect(page.getByTestId("add-connection-error")).toHaveText('Error occured while verifying credentials!');
});
});
});
});

View File

@@ -1,38 +1,57 @@
const { request } = require('@playwright/test');
const { publicTest, expect } = require('../../fixtures/index');
const { AdminUsersPage } = require('../../fixtures/admin/users-page');
const { MyProfilePage } = require('../../fixtures/my-profile-page');
const { LoginPage } = require('../../fixtures/login-page');
const { addUser, acceptInvitation } = require('../../helpers/user-api-helper');
const { getToken } = require('../../helpers/auth-api-helper');
publicTest.describe('My Profile', () => {
let testUser;
publicTest.beforeEach(
async ({ adminCreateUserPage, loginPage, page }) => {
let addUserResponse;
const apiRequest = await request.newContext();
async ({ acceptInvitationPage, adminCreateUserPage, loginPage, page }) => {
let acceptInvitationLink;
adminCreateUserPage.seed(
Math.ceil(Math.random() * Number.MAX_SAFE_INTEGER)
);
testUser = adminCreateUserPage.generateUser();
const adminUsersPage = new AdminUsersPage(page);
const myProfilePage = new MyProfilePage(page);
await publicTest.step('login as Admin', async () => {
await loginPage.login();
await expect(loginPage.page).toHaveURL('/flows');
});
await publicTest.step('create new user', async () => {
const tokenJsonResponse = await getToken(apiRequest);
addUserResponse = await addUser(
apiRequest,
tokenJsonResponse.data.token,
{
fullName: testUser.fullName,
email: testUser.email,
}
await adminUsersPage.navigateTo();
await adminUsersPage.createUserButton.click();
await adminCreateUserPage.fullNameInput.fill(testUser.fullName);
await adminCreateUserPage.emailInput.fill(testUser.email);
await adminCreateUserPage.roleInput.click();
await adminCreateUserPage.page
.getByRole('option', { name: 'Admin' })
.click();
await adminCreateUserPage.createButton.click();
const snackbar = await adminUsersPage.getSnackbarData(
'snackbar-create-user-success'
);
await expect(snackbar.variant).toBe('success');
});
await publicTest.step('copy invitation link', async () => {
const invitationMessage =
await adminCreateUserPage.acceptInvitationLink;
acceptInvitationLink = await invitationMessage.getAttribute('href');
});
await publicTest.step('logout', async () => {
await myProfilePage.logout();
});
await publicTest.step('accept invitation', async () => {
let acceptToken = addUserResponse.data.acceptInvitationUrl.split('=')[1];
await acceptInvitation(apiRequest, {token:acceptToken, password:LoginPage.defaultPassword});
await page.goto(acceptInvitationLink);
await acceptInvitationPage.acceptInvitation(LoginPage.defaultPassword);
});
await publicTest.step('login as new Admin', async () => {

File diff suppressed because it is too large Load Diff

View File

@@ -18,7 +18,6 @@
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"axios": "^1.6.0",
"clipboard-copy": "^4.0.1",
"compare-versions": "^4.1.3",
"lodash": "^4.17.21",

View File

@@ -112,7 +112,7 @@ export default function ResetPasswordForm() {
<Alert
data-test="accept-invitation-form-error"
severity="error"
sx={{ mt: 1 }}
sx={{ mt: 1, fontWeight: 500 }}
>
{formatMessage('acceptInvitationForm.invalidToken')}
</Alert>

View File

@@ -126,7 +126,7 @@ function AddAppConnection(props) {
</DialogTitle>
{authDocUrl && (
<Alert severity="info">
<Alert severity="info" sx={{ fontWeight: 300 }}>
{formatMessage('addAppConnection.callToDocs', {
appName: name,
docsLink: generateExternalLink(authDocUrl),
@@ -138,7 +138,7 @@ function AddAppConnection(props) {
<Alert
data-test="add-connection-error"
severity="error"
sx={{ mt: 1, wordBreak: 'break-all' }}
sx={{ mt: 1, fontWeight: 500, wordBreak: 'break-all' }}
>
{!errorDetails && errorMessage}
{errorDetails && (

View File

@@ -32,7 +32,10 @@ function AdminApplicationAuthClientDialog(props) {
<Dialog open={true} onClose={onClose}>
<DialogTitle>{title}</DialogTitle>
{error && (
<Alert severity="error" sx={{ mt: 1, wordBreak: 'break-all' }}>
<Alert
severity="error"
sx={{ mt: 1, fontWeight: 500, wordBreak: 'break-all' }}
>
{error.message}
</Alert>
)}

View File

@@ -6,7 +6,7 @@ import FormHelperText from '@mui/material/FormHelperText';
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
import ClearIcon from '@mui/icons-material/Clear';
import { ActionButtonsWrapper } from './style';
import { ClickAwayListener } from '@mui/base/ClickAwayListener';
import ClickAwayListener from '@mui/base/ClickAwayListener';
import InputLabel from '@mui/material/InputLabel';
import { createEditor } from 'slate';
import { Editable, ReactEditor } from 'slate-react';

View File

@@ -1,8 +1,8 @@
import * as React from 'react';
import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography';
import Alert from '@mui/material/Alert';
import LoadingButton from '@mui/lab/LoadingButton';
import { enqueueSnackbar } from 'notistack';
import useForgotPassword from 'hooks/useForgotPassword';
import Form from 'components/Form';
@@ -12,17 +12,25 @@ import useFormatMessage from 'hooks/useFormatMessage';
export default function ForgotPasswordForm() {
const formatMessage = useFormatMessage();
const {
mutate: forgotPassword,
mutateAsync: forgotPassword,
isPending: loading,
isSuccess,
isError,
error,
} = useForgotPassword();
const handleSubmit = ({ email }) => {
forgotPassword({
email,
});
const handleSubmit = async (values) => {
const { email } = values;
try {
await forgotPassword({
email,
});
} catch (error) {
enqueueSnackbar(
error?.message || formatMessage('forgotPasswordForm.error'),
{
variant: 'error',
},
);
}
};
return (
@@ -49,16 +57,6 @@ export default function ForgotPasswordForm() {
margin="dense"
autoComplete="username"
/>
{isError && (
<Alert severity="error" sx={{ mt: 2 }}>
{error?.message || formatMessage('forgotPasswordForm.error')}
</Alert>
)}
{isSuccess && (
<Alert severity="success" sx={{ mt: 2 }}>
{formatMessage('forgotPasswordForm.instructionsSent')}
</Alert>
)}
<LoadingButton
type="submit"
variant="contained"
@@ -70,6 +68,14 @@ export default function ForgotPasswordForm() {
>
{formatMessage('forgotPasswordForm.submit')}
</LoadingButton>
{isSuccess && (
<Typography
variant="body1"
sx={{ color: (theme) => theme.palette.success.main }}
>
{formatMessage('forgotPasswordForm.instructionsSent')}
</Typography>
)}
</Form>
</Paper>
);

View File

@@ -188,7 +188,7 @@ function InstallationForm() {
)}
/>
{install.isSuccess && (
<Alert data-test="success-alert" severity="success" sx={{ mt: 3 }}>
<Alert data-test="success-alert" severity="success" sx={{ mt: 3, fontWeight: 500 }}>
{formatMessage('installationForm.success', {
link: (str) => (
<Link

View File

@@ -2,7 +2,6 @@ import * as React from 'react';
import { useNavigate, Link as RouterLink } from 'react-router-dom';
import Paper from '@mui/material/Paper';
import Link from '@mui/material/Link';
import Alert from '@mui/material/Alert';
import Typography from '@mui/material/Typography';
import LoadingButton from '@mui/lab/LoadingButton';
import useAuthentication from 'hooks/useAuthentication';
@@ -12,18 +11,16 @@ import Form from 'components/Form';
import TextField from 'components/TextField';
import useFormatMessage from 'hooks/useFormatMessage';
import useCreateAccessToken from 'hooks/useCreateAccessToken';
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
function LoginForm() {
const isCloud = useCloud();
const navigate = useNavigate();
const formatMessage = useFormatMessage();
const enqueueSnackbar = useEnqueueSnackbar();
const authentication = useAuthentication();
const {
mutateAsync: createAccessToken,
isPending: loading,
error,
isError,
} = useCreateAccessToken();
const { mutateAsync: createAccessToken, isPending: loading } =
useCreateAccessToken();
React.useEffect(() => {
if (authentication.isAuthenticated) {
@@ -40,19 +37,23 @@ function LoginForm() {
});
const { token } = data;
authentication.updateToken(token);
} catch {}
};
} catch (error) {
const errors = error?.response?.data?.errors
? Object.values(error.response.data.errors)
: [];
const renderError = () => {
const errors = error?.response?.data?.errors?.general || [
error?.message || formatMessage('loginForm.error'),
];
return errors.map((error) => (
<Alert severity="error" sx={{ mt: 2 }}>
{error}
</Alert>
));
if (errors.length) {
for (const [error] of errors) {
enqueueSnackbar(error, {
variant: 'error',
});
}
} else {
enqueueSnackbar(error?.message || formatMessage('loginForm.error'), {
variant: 'error',
});
}
}
};
return (
@@ -105,8 +106,6 @@ function LoginForm() {
</Link>
)}
{isError && renderError()}
<LoadingButton
type="submit"
variant="contained"

View File

@@ -30,7 +30,7 @@ const PermissionCatalogField = ({
if (isPermissionCatalogLoading) return <PermissionCatalogFieldLoader />;
return (
<TableContainer data-test="permissions-catalog" component={Paper}>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>

View File

@@ -1,5 +1,5 @@
import PropTypes from 'prop-types';
import { ClickAwayListener } from '@mui/base/ClickAwayListener';
import ClickAwayListener from '@mui/base/ClickAwayListener';
import FormHelperText from '@mui/material/FormHelperText';
import InputLabel from '@mui/material/InputLabel';
import * as React from 'react';

View File

@@ -2,7 +2,6 @@ import { yupResolver } from '@hookform/resolvers/yup';
import LoadingButton from '@mui/lab/LoadingButton';
import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography';
import Alert from '@mui/material/Alert';
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
import * as React from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
@@ -31,8 +30,6 @@ export default function ResetPasswordForm() {
mutateAsync: resetPassword,
isPending,
isSuccess,
error,
isError,
} = useResetPassword();
const token = searchParams.get('token');
@@ -50,23 +47,14 @@ export default function ResetPasswordForm() {
},
});
navigate(URLS.LOGIN);
} catch {}
};
const renderError = () => {
if (!isError) {
return null;
} catch (error) {
enqueueSnackbar(
error?.message || formatMessage('resetPasswordForm.error'),
{
variant: 'error',
},
);
}
const errors = error?.response?.data?.errors?.general || [
error?.message || formatMessage('resetPasswordForm.error'),
];
return errors.map((error) => (
<Alert severity="error" sx={{ mt: 2 }}>
{error}
</Alert>
));
};
return (
@@ -108,6 +96,7 @@ export default function ResetPasswordForm() {
: ''
}
/>
<TextField
label={formatMessage(
'resetPasswordForm.confirmPasswordFieldLabel',
@@ -128,7 +117,7 @@ export default function ResetPasswordForm() {
: ''
}
/>
{renderError()}
<LoadingButton
type="submit"
variant="contained"

View File

@@ -7,7 +7,7 @@ import FormControl from '@mui/material/FormControl';
import SearchIcon from '@mui/icons-material/Search';
import useFormatMessage from 'hooks/useFormatMessage';
export default function SearchInput({ onChange, defaultValue = '' }) {
export default function SearchInput({ onChange }) {
const formatMessage = useFormatMessage();
return (
<FormControl variant="outlined" fullWidth>
@@ -16,7 +16,6 @@ export default function SearchInput({ onChange, defaultValue = '' }) {
</InputLabel>
<OutlinedInput
defaultValue={defaultValue}
id="search-input"
type="text"
size="medium"
@@ -35,5 +34,4 @@ export default function SearchInput({ onChange, defaultValue = '' }) {
SearchInput.propTypes = {
onChange: PropTypes.func,
defaultValue: PropTypes.string,
};

View File

@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
import Button from '@mui/material/Button';
import ButtonGroup from '@mui/material/ButtonGroup';
import { ClickAwayListener } from '@mui/base/ClickAwayListener';
import ClickAwayListener from '@mui/material/ClickAwayListener';
import Grow from '@mui/material/Grow';
import MenuItem from '@mui/material/MenuItem';
import MenuList from '@mui/material/MenuList';

View File

@@ -84,7 +84,10 @@ function TestSubstep(props) {
}}
>
{hasError && (
<Alert severity="error" sx={{ mb: 2, width: '100%' }}>
<Alert
severity="error"
sx={{ mb: 2, fontWeight: 500, width: '100%' }}
>
<pre style={{ margin: 0, whiteSpace: 'pre-wrap' }}>
{JSON.stringify(errorDetails, null, 2)}
</pre>
@@ -101,11 +104,13 @@ function TestSubstep(props) {
severity="warning"
sx={{ mb: 1, width: '100%' }}
>
<AlertTitle>
<AlertTitle sx={{ fontWeight: 700 }}>
{formatMessage('flowEditor.noTestDataTitle')}
</AlertTitle>
<Box>{formatMessage('flowEditor.noTestDataMessage')}</Box>
<Box sx={{ fontWeight: 400 }}>
{formatMessage('flowEditor.noTestDataMessage')}
</Box>
</Alert>
)}

View File

@@ -124,6 +124,7 @@ export default function CreateUser() {
<Alert
severity="info"
color="primary"
sx={{ fontWeight: '500' }}
data-test="invitation-email-info-alert"
>
{formatMessage('createUser.invitationEmailInfo', {

View File

@@ -42,9 +42,13 @@ export default function Execution() {
<Grid container item sx={{ mt: 2, mb: [2, 5] }} rowGap={3}>
{!isExecutionStepsLoading && !data?.pages?.[0].data.length && (
<Alert severity="warning" sx={{ flex: 1 }}>
<AlertTitle>{formatMessage('execution.noDataTitle')}</AlertTitle>
<AlertTitle sx={{ fontWeight: 700 }}>
{formatMessage('execution.noDataTitle')}
</AlertTitle>
<Box>{formatMessage('execution.noDataMessage')}</Box>
<Box sx={{ fontWeight: 400 }}>
{formatMessage('execution.noDataMessage')}
</Box>
</Alert>
)}

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
import { Link, useSearchParams } from 'react-router-dom';
import debounce from 'lodash/debounce';
import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid';
@@ -23,18 +23,13 @@ import useLazyFlows from 'hooks/useLazyFlows';
export default function Flows() {
const formatMessage = useFormatMessage();
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const page = parseInt(searchParams.get('page') || '', 10) || 1;
const flowName = searchParams.get('flowName') || '';
const [isLoading, setIsLoading] = React.useState(true);
const [flowName, setFlowName] = React.useState('');
const [isLoading, setIsLoading] = React.useState(false);
const currentUserAbility = useCurrentUserAbility();
const {
data,
mutate: fetchFlows,
isSuccess,
} = useLazyFlows(
const { data, mutate: fetchFlows } = useLazyFlows(
{ flowName, page },
{
onSettled: () => {
@@ -43,36 +38,6 @@ export default function Flows() {
},
);
const flows = data?.data || [];
const pageInfo = data?.meta;
const hasFlows = flows?.length;
const navigateToLastPage = isSuccess && !hasFlows && page > 1;
const onSearchChange = React.useCallback((event) => {
setSearchParams({ flowName: event.target.value });
}, []);
const getPathWithSearchParams = (page, flowName) => {
const searchParams = new URLSearchParams();
if (page > 1) {
searchParams.set('page', page);
}
if (flowName) {
searchParams.set('flowName', flowName);
}
return { search: searchParams.toString() };
};
const onDuplicateFlow = () => {
if (pageInfo?.currentPage > 1) {
navigate(getPathWithSearchParams(1, flowName));
} else {
fetchFlows();
}
};
const fetchData = React.useMemo(
() => debounce(fetchFlows, 300),
[fetchFlows],
@@ -89,14 +54,21 @@ export default function Flows() {
}, [fetchData, flowName, page]);
React.useEffect(
function redirectToLastPage() {
if (navigateToLastPage) {
navigate(getPathWithSearchParams(pageInfo.totalPages, flowName));
}
function resetPageOnSearch() {
// reset search params which only consists of `page`
setSearchParams({});
},
[navigateToLastPage],
[flowName],
);
const flows = data?.data || [];
const pageInfo = data?.meta;
const hasFlows = flows?.length;
const onSearchChange = React.useCallback((event) => {
setFlowName(event.target.value);
}, []);
return (
<Box sx={{ py: 3 }}>
<Container>
@@ -106,7 +78,7 @@ export default function Flows() {
</Grid>
<Grid item xs={12} sm="auto" order={{ xs: 2, sm: 1 }}>
<SearchInput onChange={onSearchChange} defaultValue={flowName} />
<SearchInput onChange={onSearchChange} />
</Grid>
<Grid
@@ -139,7 +111,7 @@ export default function Flows() {
</Grid>
<Divider sx={{ mt: [2, 0], mb: 2 }} />
{(isLoading || navigateToLastPage) && (
{isLoading && (
<CircularProgress sx={{ display: 'block', margin: '20px auto' }} />
)}
{!isLoading &&
@@ -147,11 +119,11 @@ export default function Flows() {
<FlowRow
key={flow.id}
flow={flow}
onDuplicateFlow={onDuplicateFlow}
onDuplicateFlow={fetchFlows}
onDeleteFlow={fetchFlows}
/>
))}
{!isLoading && !navigateToLastPage && !hasFlows && (
{!isLoading && !hasFlows && (
<NoResultFound
text={formatMessage('flows.noFlows')}
{...(currentUserAbility.can('create', 'Flow') && {
@@ -159,23 +131,23 @@ export default function Flows() {
})}
/>
)}
{!isLoading &&
!navigateToLastPage &&
pageInfo &&
pageInfo.totalPages > 1 && (
<Pagination
sx={{ display: 'flex', justifyContent: 'center', mt: 3 }}
page={pageInfo?.currentPage}
count={pageInfo?.totalPages}
renderItem={(item) => (
<PaginationItem
component={Link}
to={getPathWithSearchParams(item.page, flowName)}
{...item}
/>
)}
/>
)}
{!isLoading && pageInfo && pageInfo.totalPages > 1 && (
<Pagination
sx={{ display: 'flex', justifyContent: 'center', mt: 3 }}
page={pageInfo?.currentPage}
count={pageInfo?.totalPages}
onChange={(event, page) =>
setSearchParams({ page: page.toString() })
}
renderItem={(item) => (
<PaginationItem
component={Link}
to={`${item.page === 1 ? '' : `?page=${item.page}`}`}
{...item}
/>
)}
/>
)}
</Container>
</Box>
);

View File

@@ -266,8 +266,8 @@ function ProfileSettings() {
</Grid>
<Grid item xs={12} justifyContent="flex-end" sx={{ pt: 5 }}>
<Alert variant="outlined" severity="error">
<AlertTitle>
<Alert variant="outlined" severity="error" sx={{ fontWeight: 500 }}>
<AlertTitle sx={{ fontWeight: 700 }}>
{formatMessage('profileSettings.deleteMyAccount')}
</AlertTitle>

View File

@@ -278,20 +278,6 @@ export const defaultTheme = createTheme({
}),
},
},
MuiAlert: {
styleOverrides: {
root: ({ theme }) => ({
fontWeight: theme.typography.fontWeightRegular,
}),
},
},
MuiAlertTitle: {
styleOverrides: {
root: ({ theme }) => ({
fontWeight: theme.typography.fontWeightBold,
}),
},
},
},
});
export const mationTheme = createTheme(

File diff suppressed because it is too large Load Diff

17164
yarn.lock Normal file

File diff suppressed because it is too large Load Diff