Compare commits
48 Commits
test-cover
...
AUT-1376-n
Author | SHA1 | Date | |
---|---|---|---|
![]() |
53d95366e9 | ||
![]() |
7a8dc68592 | ||
![]() |
978ceaadb6 | ||
![]() |
770b07179f | ||
![]() |
6d15167ad9 | ||
![]() |
39cba6bc74 | ||
![]() |
9558e66abf | ||
![]() |
ff7908955e | ||
![]() |
26b095b835 | ||
![]() |
feba2a32f9 | ||
![]() |
5090ece9b6 | ||
![]() |
221b19586e | ||
![]() |
3346c14255 | ||
![]() |
6e97e023c9 | ||
![]() |
b26e2ecf2e | ||
![]() |
d896238f23 | ||
![]() |
d2c8f5a75c | ||
![]() |
ce430d238c | ||
![]() |
ee397441ed | ||
![]() |
ba82d986c1 | ||
![]() |
2361cb521e | ||
![]() |
05f8d95281 | ||
![]() |
6c60b1c263 | ||
![]() |
0c32a0693c | ||
![]() |
807faa3c93 | ||
![]() |
fb53e37f7a | ||
![]() |
4ffdf98e16 | ||
![]() |
b8da721e39 | ||
![]() |
db8b98ca16 | ||
![]() |
01b8c600fe | ||
![]() |
69bd5549a2 | ||
![]() |
bc631e3931 | ||
![]() |
8ca4bc5a33 | ||
![]() |
58a569afb0 | ||
![]() |
db718d6fc3 | ||
![]() |
ca9cb8b07b | ||
![]() |
ef14586412 | ||
![]() |
15f1fca6fe | ||
![]() |
a570b8eb7a | ||
![]() |
02e2735b7a | ||
![]() |
54fa347142 | ||
![]() |
0c752beace | ||
![]() |
c14f808d29 | ||
![]() |
ad71173671 | ||
![]() |
204325ef44 | ||
![]() |
7ce6117659 | ||
![]() |
551548400f | ||
![]() |
90a7b4c1c0 |
13
.github/workflows/playwright.yml
vendored
13
.github/workflows/playwright.yml
vendored
@@ -3,12 +3,13 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
pull_request:
|
# TODO: Add pull request after optimizing the total excecution time of the test suite.
|
||||||
paths:
|
# pull_request:
|
||||||
- 'packages/backend/**'
|
# paths:
|
||||||
- 'packages/e2e-tests/**'
|
# - 'packages/backend/**'
|
||||||
- 'packages/web/**'
|
# - 'packages/e2e-tests/**'
|
||||||
- '!packages/backend/src/apps/**'
|
# - 'packages/web/**'
|
||||||
|
# - '!packages/backend/src/apps/**'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
|
@@ -7,7 +7,7 @@ export default async (request, response) => {
|
|||||||
.throwIfNotFound();
|
.throwIfNotFound();
|
||||||
|
|
||||||
const roleMappings = await samlAuthProvider
|
const roleMappings = await samlAuthProvider
|
||||||
.$relatedQuery('samlAuthProvidersRoleMappings')
|
.$relatedQuery('roleMappings')
|
||||||
.orderBy('remote_role_name', 'asc');
|
.orderBy('remote_role_name', 'asc');
|
||||||
|
|
||||||
renderObject(response, roleMappings);
|
renderObject(response, roleMappings);
|
||||||
|
@@ -8,15 +8,14 @@ export default async (request, response) => {
|
|||||||
.findById(samlAuthProviderId)
|
.findById(samlAuthProviderId)
|
||||||
.throwIfNotFound();
|
.throwIfNotFound();
|
||||||
|
|
||||||
const samlAuthProvidersRoleMappings =
|
const roleMappings = await samlAuthProvider.updateRoleMappings(
|
||||||
await samlAuthProvider.updateRoleMappings(
|
roleMappingsParams(request)
|
||||||
samlAuthProvidersRoleMappingsParams(request)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
renderObject(response, samlAuthProvidersRoleMappings);
|
renderObject(response, roleMappings);
|
||||||
};
|
};
|
||||||
|
|
||||||
const samlAuthProvidersRoleMappingsParams = (request) => {
|
const roleMappingsParams = (request) => {
|
||||||
const roleMappings = request.body;
|
const roleMappings = request.body;
|
||||||
|
|
||||||
return roleMappings.map(({ roleId, remoteRoleName }) => ({
|
return roleMappings.map(({ roleId, remoteRoleName }) => ({
|
||||||
|
@@ -6,7 +6,7 @@ import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by
|
|||||||
import { createRole } from '../../../../../../test/factories/role.js';
|
import { createRole } from '../../../../../../test/factories/role.js';
|
||||||
import { createUser } from '../../../../../../test/factories/user.js';
|
import { createUser } from '../../../../../../test/factories/user.js';
|
||||||
import { createSamlAuthProvider } from '../../../../../../test/factories/saml-auth-provider.ee.js';
|
import { createSamlAuthProvider } from '../../../../../../test/factories/saml-auth-provider.ee.js';
|
||||||
import { createSamlAuthProvidersRoleMapping } from '../../../../../../test/factories/saml-auth-providers-role-mapping.js';
|
import { createRoleMapping } from '../../../../../../test/factories/role-mapping.js';
|
||||||
import createRoleMappingsMock from '../../../../../../test/mocks/rest/api/v1/admin/saml-auth-providers/update-role-mappings.ee.js';
|
import createRoleMappingsMock from '../../../../../../test/mocks/rest/api/v1/admin/saml-auth-providers/update-role-mappings.ee.js';
|
||||||
import * as license from '../../../../../helpers/license.ee.js';
|
import * as license from '../../../../../helpers/license.ee.js';
|
||||||
|
|
||||||
@@ -21,12 +21,12 @@ describe('PATCH /api/v1/admin/saml-auth-providers/:samlAuthProviderId/role-mappi
|
|||||||
|
|
||||||
samlAuthProvider = await createSamlAuthProvider();
|
samlAuthProvider = await createSamlAuthProvider();
|
||||||
|
|
||||||
await createSamlAuthProvidersRoleMapping({
|
await createRoleMapping({
|
||||||
samlAuthProviderId: samlAuthProvider.id,
|
samlAuthProviderId: samlAuthProvider.id,
|
||||||
remoteRoleName: 'Viewer',
|
remoteRoleName: 'Viewer',
|
||||||
});
|
});
|
||||||
|
|
||||||
await createSamlAuthProvidersRoleMapping({
|
await createRoleMapping({
|
||||||
samlAuthProviderId: samlAuthProvider.id,
|
samlAuthProviderId: samlAuthProvider.id,
|
||||||
remoteRoleName: 'Editor',
|
remoteRoleName: 'Editor',
|
||||||
});
|
});
|
||||||
@@ -64,7 +64,7 @@ describe('PATCH /api/v1/admin/saml-auth-providers/:samlAuthProviderId/role-mappi
|
|||||||
|
|
||||||
it('should delete role mappings when given empty role mappings', async () => {
|
it('should delete role mappings when given empty role mappings', async () => {
|
||||||
const existingRoleMappings = await samlAuthProvider.$relatedQuery(
|
const existingRoleMappings = await samlAuthProvider.$relatedQuery(
|
||||||
'samlAuthProvidersRoleMappings'
|
'roleMappings'
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(existingRoleMappings.length).toBe(2);
|
expect(existingRoleMappings.length).toBe(2);
|
||||||
@@ -149,34 +149,4 @@ describe('PATCH /api/v1/admin/saml-auth-providers/:samlAuthProviderId/role-mappi
|
|||||||
.send(roleMappings)
|
.send(roleMappings)
|
||||||
.expect(404);
|
.expect(404);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not delete existing role mapping when error thrown', async () => {
|
|
||||||
const roleMappings = [
|
|
||||||
{
|
|
||||||
roleId: userRole.id,
|
|
||||||
remoteRoleName: {
|
|
||||||
invalid: 'data',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const roleMappingsBeforeRequest = await samlAuthProvider.$relatedQuery(
|
|
||||||
'samlAuthProvidersRoleMappings'
|
|
||||||
);
|
|
||||||
|
|
||||||
await request(app)
|
|
||||||
.patch(
|
|
||||||
`/api/v1/admin/saml-auth-providers/${samlAuthProvider.id}/role-mappings`
|
|
||||||
)
|
|
||||||
.set('Authorization', token)
|
|
||||||
.send(roleMappings)
|
|
||||||
.expect(422);
|
|
||||||
|
|
||||||
const roleMappingsAfterRequest = await samlAuthProvider.$relatedQuery(
|
|
||||||
'samlAuthProvidersRoleMappings'
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(roleMappingsBeforeRequest).toStrictEqual(roleMappingsAfterRequest);
|
|
||||||
expect(roleMappingsAfterRequest.length).toBe(2);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@@ -87,14 +87,14 @@ describe('GET /api/v1/apps/:appKey/connections', () => {
|
|||||||
|
|
||||||
it('should return not found response for invalid connection UUID', async () => {
|
it('should return not found response for invalid connection UUID', async () => {
|
||||||
await createPermission({
|
await createPermission({
|
||||||
action: 'update',
|
action: 'read',
|
||||||
subject: 'Connection',
|
subject: 'Connection',
|
||||||
roleId: currentUserRole.id,
|
roleId: currentUserRole.id,
|
||||||
conditions: ['isCreator'],
|
conditions: ['isCreator'],
|
||||||
});
|
});
|
||||||
|
|
||||||
await request(app)
|
await request(app)
|
||||||
.get('/api/v1/connections/invalid-connection-id/connections')
|
.get('/api/v1/apps/invalid-connection-id/connections')
|
||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.expect(404);
|
.expect(404);
|
||||||
});
|
});
|
||||||
|
@@ -193,7 +193,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-data', () => {
|
|||||||
const notExistingStepUUID = Crypto.randomUUID();
|
const notExistingStepUUID = Crypto.randomUUID();
|
||||||
|
|
||||||
await request(app)
|
await request(app)
|
||||||
.get(`/api/v1/steps/${notExistingStepUUID}/dynamic-data`)
|
.post(`/api/v1/steps/${notExistingStepUUID}/dynamic-data`)
|
||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.expect(404);
|
.expect(404);
|
||||||
});
|
});
|
||||||
@@ -216,7 +216,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-data', () => {
|
|||||||
const step = await createStep({ appKey: null });
|
const step = await createStep({ appKey: null });
|
||||||
|
|
||||||
await request(app)
|
await request(app)
|
||||||
.get(`/api/v1/steps/${step.id}/dynamic-data`)
|
.post(`/api/v1/steps/${step.id}/dynamic-data`)
|
||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.expect(404);
|
.expect(404);
|
||||||
});
|
});
|
||||||
|
@@ -118,7 +118,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-fields', () => {
|
|||||||
const notExistingStepUUID = Crypto.randomUUID();
|
const notExistingStepUUID = Crypto.randomUUID();
|
||||||
|
|
||||||
await request(app)
|
await request(app)
|
||||||
.get(`/api/v1/steps/${notExistingStepUUID}/dynamic-fields`)
|
.post(`/api/v1/steps/${notExistingStepUUID}/dynamic-fields`)
|
||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.expect(404);
|
.expect(404);
|
||||||
});
|
});
|
||||||
@@ -138,10 +138,11 @@ describe('POST /api/v1/steps/:stepId/dynamic-fields', () => {
|
|||||||
conditions: [],
|
conditions: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const step = await createStep({ appKey: null });
|
const step = await createStep();
|
||||||
|
await step.$query().patch({ appKey: null });
|
||||||
|
|
||||||
await request(app)
|
await request(app)
|
||||||
.get(`/api/v1/steps/${step.id}/dynamic-fields`)
|
.post(`/api/v1/steps/${step.id}/dynamic-fields`)
|
||||||
.set('Authorization', token)
|
.set('Authorization', token)
|
||||||
.expect(404);
|
.expect(404);
|
||||||
});
|
});
|
||||||
|
@@ -0,0 +1,52 @@
|
|||||||
|
export async function up(knex) {
|
||||||
|
await knex.schema.createTable('role_mappings', (table) => {
|
||||||
|
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
|
||||||
|
table
|
||||||
|
.uuid('saml_auth_provider_id')
|
||||||
|
.references('id')
|
||||||
|
.inTable('saml_auth_providers');
|
||||||
|
table.uuid('role_id').references('id').inTable('roles');
|
||||||
|
table.string('remote_role_name').notNullable();
|
||||||
|
|
||||||
|
table.unique(['saml_auth_provider_id', 'remote_role_name']);
|
||||||
|
|
||||||
|
table.timestamps(true, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
const existingRoleMappings = await knex('saml_auth_providers_role_mappings');
|
||||||
|
|
||||||
|
if (existingRoleMappings.length) {
|
||||||
|
await knex('role_mappings').insert(existingRoleMappings);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await knex.schema.dropTable('saml_auth_providers_role_mappings');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex) {
|
||||||
|
await knex.schema.createTable(
|
||||||
|
'saml_auth_providers_role_mappings',
|
||||||
|
(table) => {
|
||||||
|
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
|
||||||
|
table
|
||||||
|
.uuid('saml_auth_provider_id')
|
||||||
|
.references('id')
|
||||||
|
.inTable('saml_auth_providers');
|
||||||
|
table.uuid('role_id').references('id').inTable('roles');
|
||||||
|
table.string('remote_role_name').notNullable();
|
||||||
|
|
||||||
|
table.unique(['saml_auth_provider_id', 'remote_role_name']);
|
||||||
|
|
||||||
|
table.timestamps(true, true);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const existingRoleMappings = await knex('role_mappings');
|
||||||
|
|
||||||
|
if (existingRoleMappings.length) {
|
||||||
|
await knex('saml_auth_providers_role_mappings').insert(
|
||||||
|
existingRoleMappings
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await knex.schema.dropTable('role_mappings');
|
||||||
|
}
|
@@ -30,7 +30,7 @@ const findOrCreateUserBySamlIdentity = async (
|
|||||||
: [mappedUser.role];
|
: [mappedUser.role];
|
||||||
|
|
||||||
const samlAuthProviderRoleMapping = await samlAuthProvider
|
const samlAuthProviderRoleMapping = await samlAuthProvider
|
||||||
.$relatedQuery('samlAuthProvidersRoleMappings')
|
.$relatedQuery('roleMappings')
|
||||||
.whereIn('remote_role_name', mappedRoles)
|
.whereIn('remote_role_name', mappedRoles)
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.first();
|
.first();
|
||||||
|
46
packages/backend/src/helpers/user-ability.test.js
Normal file
46
packages/backend/src/helpers/user-ability.test.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import userAbility from './user-ability.js';
|
||||||
|
|
||||||
|
describe('userAbility', () => {
|
||||||
|
it('should return PureAbility instantiated with user permissions', () => {
|
||||||
|
const user = {
|
||||||
|
permissions: [
|
||||||
|
{
|
||||||
|
subject: 'Flow',
|
||||||
|
action: 'read',
|
||||||
|
conditions: ['isCreator'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
role: {
|
||||||
|
name: 'User',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const ability = userAbility(user);
|
||||||
|
|
||||||
|
expect(ability.rules).toStrictEqual(user.permissions);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return permission-less PureAbility for user with no role', () => {
|
||||||
|
const user = {
|
||||||
|
permissions: [
|
||||||
|
{
|
||||||
|
subject: 'Flow',
|
||||||
|
action: 'read',
|
||||||
|
conditions: ['isCreator'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
role: null,
|
||||||
|
};
|
||||||
|
const ability = userAbility(user);
|
||||||
|
|
||||||
|
expect(ability.rules).toStrictEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return permission-less PureAbility for user with no permissions', () => {
|
||||||
|
const user = { permissions: null, role: { name: 'User' } };
|
||||||
|
const ability = userAbility(user);
|
||||||
|
|
||||||
|
expect(ability.rules).toStrictEqual([]);
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,30 @@
|
|||||||
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`RoleMapping model > jsonSchema should have the correct schema 1`] = `
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"remoteRoleName": {
|
||||||
|
"minLength": 1,
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"roleId": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"samlAuthProviderId": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"samlAuthProviderId",
|
||||||
|
"roleId",
|
||||||
|
"remoteRoleName",
|
||||||
|
],
|
||||||
|
"type": "object",
|
||||||
|
}
|
||||||
|
`;
|
@@ -1,6 +1,6 @@
|
|||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
exports[`SamlAuthProvidersRoleMapping model > jsonSchema should have the correct schema 1`] = `
|
exports[`RoleMapping model > jsonSchema should have the correct schema 1`] = `
|
||||||
{
|
{
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"id": {
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
import Base from './base.js';
|
import Base from './base.js';
|
||||||
import SamlAuthProvider from './saml-auth-provider.ee.js';
|
import SamlAuthProvider from './saml-auth-provider.ee.js';
|
||||||
|
|
||||||
class SamlAuthProvidersRoleMapping extends Base {
|
class RoleMapping extends Base {
|
||||||
static tableName = 'saml_auth_providers_role_mappings';
|
static tableName = 'role_mappings';
|
||||||
|
|
||||||
static jsonSchema = {
|
static jsonSchema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
@@ -21,11 +21,11 @@ class SamlAuthProvidersRoleMapping extends Base {
|
|||||||
relation: Base.BelongsToOneRelation,
|
relation: Base.BelongsToOneRelation,
|
||||||
modelClass: SamlAuthProvider,
|
modelClass: SamlAuthProvider,
|
||||||
join: {
|
join: {
|
||||||
from: 'saml_auth_providers_role_mappings.saml_auth_provider_id',
|
from: 'role_mappings.saml_auth_provider_id',
|
||||||
to: 'saml_auth_providers.id',
|
to: 'saml_auth_providers.id',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SamlAuthProvidersRoleMapping;
|
export default RoleMapping;
|
@@ -1,28 +1,26 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import SamlAuthProvidersRoleMapping from '../models/saml-auth-providers-role-mapping.ee';
|
import RoleMapping from './role-mapping.ee';
|
||||||
import SamlAuthProvider from './saml-auth-provider.ee';
|
import SamlAuthProvider from './saml-auth-provider.ee';
|
||||||
import Base from './base';
|
import Base from './base';
|
||||||
|
|
||||||
describe('SamlAuthProvidersRoleMapping model', () => {
|
describe('RoleMapping model', () => {
|
||||||
it('tableName should return correct name', () => {
|
it('tableName should return correct name', () => {
|
||||||
expect(SamlAuthProvidersRoleMapping.tableName).toBe(
|
expect(RoleMapping.tableName).toBe('role_mappings');
|
||||||
'saml_auth_providers_role_mappings'
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('jsonSchema should have the correct schema', () => {
|
it('jsonSchema should have the correct schema', () => {
|
||||||
expect(SamlAuthProvidersRoleMapping.jsonSchema).toMatchSnapshot();
|
expect(RoleMapping.jsonSchema).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('relationMappings should return correct associations', () => {
|
it('relationMappings should return correct associations', () => {
|
||||||
const relationMappings = SamlAuthProvidersRoleMapping.relationMappings();
|
const relationMappings = RoleMapping.relationMappings();
|
||||||
|
|
||||||
const expectedRelations = {
|
const expectedRelations = {
|
||||||
samlAuthProvider: {
|
samlAuthProvider: {
|
||||||
relation: Base.BelongsToOneRelation,
|
relation: Base.BelongsToOneRelation,
|
||||||
modelClass: SamlAuthProvider,
|
modelClass: SamlAuthProvider,
|
||||||
join: {
|
join: {
|
||||||
from: 'saml_auth_providers_role_mappings.saml_auth_provider_id',
|
from: 'role_mappings.saml_auth_provider_id',
|
||||||
to: 'saml_auth_providers.id',
|
to: 'saml_auth_providers.id',
|
||||||
},
|
},
|
||||||
},
|
},
|
@@ -5,7 +5,7 @@ import appConfig from '../config/app.js';
|
|||||||
import axios from '../helpers/axios-with-proxy.js';
|
import axios from '../helpers/axios-with-proxy.js';
|
||||||
import Base from './base.js';
|
import Base from './base.js';
|
||||||
import Identity from './identity.ee.js';
|
import Identity from './identity.ee.js';
|
||||||
import SamlAuthProvidersRoleMapping from './saml-auth-providers-role-mapping.ee.js';
|
import RoleMapping from './role-mapping.ee.js';
|
||||||
|
|
||||||
class SamlAuthProvider extends Base {
|
class SamlAuthProvider extends Base {
|
||||||
static tableName = 'saml_auth_providers';
|
static tableName = 'saml_auth_providers';
|
||||||
@@ -53,12 +53,12 @@ class SamlAuthProvider extends Base {
|
|||||||
to: 'saml_auth_providers.id',
|
to: 'saml_auth_providers.id',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
samlAuthProvidersRoleMappings: {
|
roleMappings: {
|
||||||
relation: Base.HasManyRelation,
|
relation: Base.HasManyRelation,
|
||||||
modelClass: SamlAuthProvidersRoleMapping,
|
modelClass: RoleMapping,
|
||||||
join: {
|
join: {
|
||||||
from: 'saml_auth_providers.id',
|
from: 'saml_auth_providers.id',
|
||||||
to: 'saml_auth_providers_role_mappings.saml_auth_provider_id',
|
to: 'role_mappings.saml_auth_provider_id',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -133,27 +133,22 @@ class SamlAuthProvider extends Base {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async updateRoleMappings(roleMappings) {
|
async updateRoleMappings(roleMappings) {
|
||||||
return await SamlAuthProvider.transaction(async (trx) => {
|
await this.$relatedQuery('roleMappings').delete();
|
||||||
await this.$relatedQuery('samlAuthProvidersRoleMappings', trx).delete();
|
|
||||||
|
|
||||||
if (isEmpty(roleMappings)) {
|
if (isEmpty(roleMappings)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const samlAuthProvidersRoleMappingsData = roleMappings.map(
|
const roleMappingsData = roleMappings.map((roleMapping) => ({
|
||||||
(samlAuthProvidersRoleMapping) => ({
|
...roleMapping,
|
||||||
...samlAuthProvidersRoleMapping,
|
|
||||||
samlAuthProviderId: this.id,
|
samlAuthProviderId: this.id,
|
||||||
})
|
}));
|
||||||
|
|
||||||
|
const newRoleMappings = await RoleMapping.query().insertAndFetch(
|
||||||
|
roleMappingsData
|
||||||
);
|
);
|
||||||
|
|
||||||
const samlAuthProvidersRoleMappings =
|
return newRoleMappings;
|
||||||
await SamlAuthProvidersRoleMapping.query(trx).insertAndFetch(
|
|
||||||
samlAuthProvidersRoleMappingsData
|
|
||||||
);
|
|
||||||
|
|
||||||
return samlAuthProvidersRoleMappings;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,9 +1,14 @@
|
|||||||
import { vi, describe, it, expect } from 'vitest';
|
import { vi, beforeEach, describe, it, expect } from 'vitest';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import SamlAuthProvider from '../models/saml-auth-provider.ee';
|
import SamlAuthProvider from '../models/saml-auth-provider.ee';
|
||||||
import SamlAuthProvidersRoleMapping from '../models/saml-auth-providers-role-mapping.ee';
|
import RoleMapping from '../models/role-mapping.ee';
|
||||||
|
import axios from '../helpers/axios-with-proxy.js';
|
||||||
import Identity from './identity.ee';
|
import Identity from './identity.ee';
|
||||||
import Base from './base';
|
import Base from './base';
|
||||||
import appConfig from '../config/app';
|
import appConfig from '../config/app';
|
||||||
|
import { createSamlAuthProvider } from '../../test/factories/saml-auth-provider.ee.js';
|
||||||
|
import { createRoleMapping } from '../../test/factories/role-mapping.js';
|
||||||
|
import { createRole } from '../../test/factories/role.js';
|
||||||
|
|
||||||
describe('SamlAuthProvider model', () => {
|
describe('SamlAuthProvider model', () => {
|
||||||
it('tableName should return correct name', () => {
|
it('tableName should return correct name', () => {
|
||||||
@@ -26,12 +31,12 @@ describe('SamlAuthProvider model', () => {
|
|||||||
to: 'saml_auth_providers.id',
|
to: 'saml_auth_providers.id',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
samlAuthProvidersRoleMappings: {
|
roleMappings: {
|
||||||
relation: Base.HasManyRelation,
|
relation: Base.HasManyRelation,
|
||||||
modelClass: SamlAuthProvidersRoleMapping,
|
modelClass: RoleMapping,
|
||||||
join: {
|
join: {
|
||||||
from: 'saml_auth_providers.id',
|
from: 'saml_auth_providers.id',
|
||||||
to: 'saml_auth_providers_role_mappings.saml_auth_provider_id',
|
to: 'role_mappings.saml_auth_provider_id',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -81,4 +86,146 @@ describe('SamlAuthProvider model', () => {
|
|||||||
'https://example.com/saml/logout'
|
'https://example.com/saml/logout'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('config should return the correct configuration object', () => {
|
||||||
|
const samlAuthProvider = new SamlAuthProvider();
|
||||||
|
|
||||||
|
samlAuthProvider.certificate = 'sample-certificate';
|
||||||
|
samlAuthProvider.signatureAlgorithm = 'sha256';
|
||||||
|
samlAuthProvider.entryPoint = 'https://example.com/saml';
|
||||||
|
samlAuthProvider.issuer = 'sample-issuer';
|
||||||
|
|
||||||
|
vi.spyOn(appConfig, 'baseUrl', 'get').mockReturnValue(
|
||||||
|
'https://automatisch.io'
|
||||||
|
);
|
||||||
|
|
||||||
|
const expectedConfig = {
|
||||||
|
callbackUrl: 'https://automatisch.io/login/saml/sample-issuer/callback',
|
||||||
|
cert: 'sample-certificate',
|
||||||
|
entryPoint: 'https://example.com/saml',
|
||||||
|
issuer: 'sample-issuer',
|
||||||
|
signatureAlgorithm: 'sha256',
|
||||||
|
logoutUrl: 'https://example.com/saml',
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(samlAuthProvider.config).toStrictEqual(expectedConfig);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('generateLogoutRequestBody should return a correctly encoded SAML logout request', () => {
|
||||||
|
vi.mock('uuid', () => ({
|
||||||
|
v4: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const samlAuthProvider = new SamlAuthProvider();
|
||||||
|
|
||||||
|
samlAuthProvider.entryPoint = 'https://example.com/saml';
|
||||||
|
samlAuthProvider.issuer = 'sample-issuer';
|
||||||
|
|
||||||
|
const mockUuid = '123e4567-e89b-12d3-a456-426614174000';
|
||||||
|
uuidv4.mockReturnValue(mockUuid);
|
||||||
|
|
||||||
|
const sessionId = 'test-session-id';
|
||||||
|
|
||||||
|
const logoutRequest = samlAuthProvider.generateLogoutRequestBody(sessionId);
|
||||||
|
|
||||||
|
const expectedLogoutRequest = `
|
||||||
|
<samlp:LogoutRequest
|
||||||
|
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
|
||||||
|
ID="${mockUuid}"
|
||||||
|
Version="2.0"
|
||||||
|
IssueInstant="${new Date().toISOString()}"
|
||||||
|
Destination="https://example.com/saml">
|
||||||
|
|
||||||
|
<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">sample-issuer</saml:Issuer>
|
||||||
|
<samlp:SessionIndex>test-session-id</samlp:SessionIndex>
|
||||||
|
</samlp:LogoutRequest>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const expectedEncodedRequest = Buffer.from(expectedLogoutRequest).toString(
|
||||||
|
'base64'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(logoutRequest).toBe(expectedEncodedRequest);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('terminateRemoteSession should send the correct POST request and return the response', async () => {
|
||||||
|
vi.mock('../helpers/axios-with-proxy.js', () => ({
|
||||||
|
default: {
|
||||||
|
post: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const samlAuthProvider = new SamlAuthProvider();
|
||||||
|
|
||||||
|
samlAuthProvider.entryPoint = 'https://example.com/saml';
|
||||||
|
samlAuthProvider.generateLogoutRequestBody = vi
|
||||||
|
.fn()
|
||||||
|
.mockReturnValue('mockEncodedLogoutRequest');
|
||||||
|
|
||||||
|
const sessionId = 'test-session-id';
|
||||||
|
|
||||||
|
const mockResponse = { data: 'Logout Successful' };
|
||||||
|
axios.post.mockResolvedValue(mockResponse);
|
||||||
|
|
||||||
|
const response = await samlAuthProvider.terminateRemoteSession(sessionId);
|
||||||
|
|
||||||
|
expect(samlAuthProvider.generateLogoutRequestBody).toHaveBeenCalledWith(
|
||||||
|
sessionId
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(axios.post).toHaveBeenCalledWith(
|
||||||
|
'https://example.com/saml',
|
||||||
|
'SAMLRequest=mockEncodedLogoutRequest',
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response).toBe(mockResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateRoleMappings', () => {
|
||||||
|
let samlAuthProvider;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
samlAuthProvider = await createSamlAuthProvider();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove all existing role mappings', async () => {
|
||||||
|
await createRoleMapping({
|
||||||
|
samlAuthProviderId: samlAuthProvider.id,
|
||||||
|
remoteRoleName: 'Admin',
|
||||||
|
});
|
||||||
|
|
||||||
|
await createRoleMapping({
|
||||||
|
samlAuthProviderId: samlAuthProvider.id,
|
||||||
|
remoteRoleName: 'User',
|
||||||
|
});
|
||||||
|
|
||||||
|
await samlAuthProvider.updateRoleMappings([]);
|
||||||
|
|
||||||
|
const roleMappings = await samlAuthProvider.$relatedQuery('roleMappings');
|
||||||
|
expect(roleMappings).toStrictEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the updated role mappings when new ones are provided', async () => {
|
||||||
|
const adminRole = await createRole({ name: 'Admin' });
|
||||||
|
const userRole = await createRole({ name: 'User' });
|
||||||
|
|
||||||
|
const newRoleMappings = [
|
||||||
|
{ remoteRoleName: 'Admin', roleId: adminRole.id },
|
||||||
|
{ remoteRoleName: 'User', roleId: userRole.id },
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = await samlAuthProvider.updateRoleMappings(newRoleMappings);
|
||||||
|
|
||||||
|
const refetchedRoleMappings = await samlAuthProvider.$relatedQuery(
|
||||||
|
'roleMappings'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toStrictEqual(refetchedRoleMappings);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -212,6 +212,10 @@ class User extends Base {
|
|||||||
return `${appConfig.webAppUrl}/accept-invitation?token=${this.invitationToken}`;
|
return `${appConfig.webAppUrl}/accept-invitation?token=${this.invitationToken}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get ability() {
|
||||||
|
return userAbility(this);
|
||||||
|
}
|
||||||
|
|
||||||
static async authenticate(email, password) {
|
static async authenticate(email, password) {
|
||||||
const user = await User.query().findOne({
|
const user = await User.query().findOne({
|
||||||
email: email?.toLowerCase() || null,
|
email: email?.toLowerCase() || null,
|
||||||
@@ -366,18 +370,6 @@ class User extends Base {
|
|||||||
return now.getTime() - sentAt.getTime() < fourHoursInMilliseconds;
|
return now.getTime() - sentAt.getTime() < fourHoursInMilliseconds;
|
||||||
}
|
}
|
||||||
|
|
||||||
toTestTestCoverage() {
|
|
||||||
if (!this.resetPasswordTokenSentAt) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sentAt = new Date(this.resetPasswordTokenSentAt);
|
|
||||||
const now = new Date();
|
|
||||||
const fourHoursInMilliseconds = 1000 * 60 * 60 * 4;
|
|
||||||
|
|
||||||
return now.getTime() - sentAt.getTime() < fourHoursInMilliseconds;
|
|
||||||
}
|
|
||||||
|
|
||||||
async sendInvitationEmail() {
|
async sendInvitationEmail() {
|
||||||
await this.generateInvitationToken();
|
await this.generateInvitationToken();
|
||||||
|
|
||||||
@@ -595,62 +587,6 @@ class User extends Base {
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
async $beforeInsert(queryContext) {
|
|
||||||
await super.$beforeInsert(queryContext);
|
|
||||||
|
|
||||||
this.email = this.email.toLowerCase();
|
|
||||||
await this.generateHash();
|
|
||||||
|
|
||||||
if (appConfig.isCloud) {
|
|
||||||
this.startTrialPeriod();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async $beforeUpdate(opt, queryContext) {
|
|
||||||
await super.$beforeUpdate(opt, queryContext);
|
|
||||||
|
|
||||||
if (this.email) {
|
|
||||||
this.email = this.email.toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.generateHash();
|
|
||||||
}
|
|
||||||
|
|
||||||
async $afterInsert(queryContext) {
|
|
||||||
await super.$afterInsert(queryContext);
|
|
||||||
|
|
||||||
if (appConfig.isCloud) {
|
|
||||||
await this.$relatedQuery('usageData').insert({
|
|
||||||
userId: this.id,
|
|
||||||
consumedTaskCount: 0,
|
|
||||||
nextResetAt: DateTime.now().plus({ days: 30 }).toISODate(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async $afterFind() {
|
|
||||||
if (await hasValidLicense()) return this;
|
|
||||||
|
|
||||||
if (Array.isArray(this.permissions)) {
|
|
||||||
this.permissions = this.permissions.filter((permission) => {
|
|
||||||
const restrictedSubjects = [
|
|
||||||
'App',
|
|
||||||
'Role',
|
|
||||||
'SamlAuthProvider',
|
|
||||||
'Config',
|
|
||||||
];
|
|
||||||
|
|
||||||
return !restrictedSubjects.includes(permission.subject);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
get ability() {
|
|
||||||
return userAbility(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
can(action, subject) {
|
can(action, subject) {
|
||||||
const can = this.ability.can(action, subject);
|
const can = this.ability.can(action, subject);
|
||||||
|
|
||||||
@@ -666,12 +602,68 @@ class User extends Base {
|
|||||||
return conditionMap;
|
return conditionMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
cannot(action, subject) {
|
lowercaseEmail() {
|
||||||
const cannot = this.ability.cannot(action, subject);
|
if (this.email) {
|
||||||
|
this.email = this.email.toLowerCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (cannot) throw new NotAuthorizedError();
|
async createUsageData() {
|
||||||
|
if (appConfig.isCloud) {
|
||||||
|
return await this.$relatedQuery('usageData').insertAndFetch({
|
||||||
|
userId: this.id,
|
||||||
|
consumedTaskCount: 0,
|
||||||
|
nextResetAt: DateTime.now().plus({ days: 30 }).toISODate(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return cannot;
|
async omitEnterprisePermissionsWithoutValidLicense() {
|
||||||
|
if (await hasValidLicense()) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(this.permissions)) {
|
||||||
|
this.permissions = this.permissions.filter((permission) => {
|
||||||
|
const restrictedSubjects = [
|
||||||
|
'App',
|
||||||
|
'Role',
|
||||||
|
'SamlAuthProvider',
|
||||||
|
'Config',
|
||||||
|
];
|
||||||
|
|
||||||
|
return !restrictedSubjects.includes(permission.subject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async $beforeInsert(queryContext) {
|
||||||
|
await super.$beforeInsert(queryContext);
|
||||||
|
|
||||||
|
this.lowercaseEmail();
|
||||||
|
await this.generateHash();
|
||||||
|
|
||||||
|
if (appConfig.isCloud) {
|
||||||
|
this.startTrialPeriod();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async $beforeUpdate(opt, queryContext) {
|
||||||
|
await super.$beforeUpdate(opt, queryContext);
|
||||||
|
|
||||||
|
this.lowercaseEmail();
|
||||||
|
|
||||||
|
await this.generateHash();
|
||||||
|
}
|
||||||
|
|
||||||
|
async $afterInsert(queryContext) {
|
||||||
|
await super.$afterInsert(queryContext);
|
||||||
|
|
||||||
|
await this.createUsageData();
|
||||||
|
}
|
||||||
|
|
||||||
|
async $afterFind() {
|
||||||
|
await this.omitEnterprisePermissionsWithoutValidLicense();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,8 +1,10 @@
|
|||||||
import { describe, it, expect, vi } from 'vitest';
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
import { DateTime, Duration } from 'luxon';
|
import { DateTime, Duration } from 'luxon';
|
||||||
import appConfig from '../config/app.js';
|
import appConfig from '../config/app.js';
|
||||||
|
import * as licenseModule from '../helpers/license.ee.js';
|
||||||
import Base from './base.js';
|
import Base from './base.js';
|
||||||
import AccessToken from './access-token.js';
|
import AccessToken from './access-token.js';
|
||||||
|
import Config from './config.js';
|
||||||
import Connection from './connection.js';
|
import Connection from './connection.js';
|
||||||
import Execution from './execution.js';
|
import Execution from './execution.js';
|
||||||
import Flow from './flow.js';
|
import Flow from './flow.js';
|
||||||
@@ -19,6 +21,7 @@ import {
|
|||||||
REMOVE_AFTER_30_DAYS_OR_150_JOBS,
|
REMOVE_AFTER_30_DAYS_OR_150_JOBS,
|
||||||
REMOVE_AFTER_7_DAYS_OR_50_JOBS,
|
REMOVE_AFTER_7_DAYS_OR_50_JOBS,
|
||||||
} from '../helpers/remove-job-configuration.js';
|
} from '../helpers/remove-job-configuration.js';
|
||||||
|
import * as userAbilityModule from '../helpers/user-ability.js';
|
||||||
import { createUser } from '../../test/factories/user.js';
|
import { createUser } from '../../test/factories/user.js';
|
||||||
import { createConnection } from '../../test/factories/connection.js';
|
import { createConnection } from '../../test/factories/connection.js';
|
||||||
import { createRole } from '../../test/factories/role.js';
|
import { createRole } from '../../test/factories/role.js';
|
||||||
@@ -26,6 +29,9 @@ import { createPermission } from '../../test/factories/permission.js';
|
|||||||
import { createFlow } from '../../test/factories/flow.js';
|
import { createFlow } from '../../test/factories/flow.js';
|
||||||
import { createStep } from '../../test/factories/step.js';
|
import { createStep } from '../../test/factories/step.js';
|
||||||
import { createExecution } from '../../test/factories/execution.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', () => {
|
describe('User model', () => {
|
||||||
it('tableName should return correct name', () => {
|
it('tableName should return correct name', () => {
|
||||||
@@ -201,64 +207,6 @@ describe('User model', () => {
|
|||||||
expect(virtualAttributes).toStrictEqual(expectedAttributes);
|
expect(virtualAttributes).toStrictEqual(expectedAttributes);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('acceptInvitationUrl should return accept invitation page URL with invitation token', async () => {
|
|
||||||
const user = new User();
|
|
||||||
user.invitationToken = 'invitation-token';
|
|
||||||
|
|
||||||
vi.spyOn(appConfig, 'webAppUrl', 'get').mockReturnValue(
|
|
||||||
'https://automatisch.io'
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(user.acceptInvitationUrl).toBe(
|
|
||||||
'https://automatisch.io/accept-invitation?token=invitation-token'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('authenticate', () => {
|
|
||||||
it('should create and return the token for correct email and password', async () => {
|
|
||||||
const user = await createUser({
|
|
||||||
email: 'test-user@automatisch.io',
|
|
||||||
password: 'sample-password',
|
|
||||||
});
|
|
||||||
|
|
||||||
const token = await User.authenticate(
|
|
||||||
'test-user@automatisch.io',
|
|
||||||
'sample-password'
|
|
||||||
);
|
|
||||||
|
|
||||||
const persistedToken = await AccessToken.query().findOne({
|
|
||||||
userId: user.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(token).toBe(persistedToken.token);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return undefined for existing email and incorrect password', async () => {
|
|
||||||
await createUser({
|
|
||||||
email: 'test-user@automatisch.io',
|
|
||||||
password: 'sample-password',
|
|
||||||
});
|
|
||||||
|
|
||||||
const token = await User.authenticate(
|
|
||||||
'test-user@automatisch.io',
|
|
||||||
'wrong-password'
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(token).toBe(undefined);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return undefined for non-existing email', async () => {
|
|
||||||
await createUser({
|
|
||||||
email: 'test-user@automatisch.io',
|
|
||||||
password: 'sample-password',
|
|
||||||
});
|
|
||||||
|
|
||||||
const token = await User.authenticate('non-existing-user@automatisch.io');
|
|
||||||
|
|
||||||
expect(token).toBe(undefined);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('authorizedFlows', () => {
|
describe('authorizedFlows', () => {
|
||||||
it('should return user flows with isCreator condition', async () => {
|
it('should return user flows with isCreator condition', async () => {
|
||||||
const userRole = await createRole({ name: 'User' });
|
const userRole = await createRole({ name: 'User' });
|
||||||
@@ -501,6 +449,76 @@ describe('User model', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('acceptInvitationUrl should return accept invitation page URL with invitation token', async () => {
|
||||||
|
const user = new User();
|
||||||
|
user.invitationToken = 'invitation-token';
|
||||||
|
|
||||||
|
vi.spyOn(appConfig, 'webAppUrl', 'get').mockReturnValue(
|
||||||
|
'https://automatisch.io'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(user.acceptInvitationUrl).toBe(
|
||||||
|
'https://automatisch.io/accept-invitation?token=invitation-token'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ability should return userAbility for the user', () => {
|
||||||
|
const user = new User();
|
||||||
|
user.fullName = 'Sample user';
|
||||||
|
|
||||||
|
const userAbilitySpy = vi
|
||||||
|
.spyOn(userAbilityModule, 'default')
|
||||||
|
.mockReturnValue('user-ability');
|
||||||
|
|
||||||
|
expect(user.ability).toStrictEqual('user-ability');
|
||||||
|
expect(userAbilitySpy).toHaveBeenNthCalledWith(1, user);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('authenticate', () => {
|
||||||
|
it('should create and return the token for correct email and password', async () => {
|
||||||
|
const user = await createUser({
|
||||||
|
email: 'test-user@automatisch.io',
|
||||||
|
password: 'sample-password',
|
||||||
|
});
|
||||||
|
|
||||||
|
const token = await User.authenticate(
|
||||||
|
'test-user@automatisch.io',
|
||||||
|
'sample-password'
|
||||||
|
);
|
||||||
|
|
||||||
|
const persistedToken = await AccessToken.query().findOne({
|
||||||
|
userId: user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(token).toBe(persistedToken.token);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined for existing email and incorrect password', async () => {
|
||||||
|
await createUser({
|
||||||
|
email: 'test-user@automatisch.io',
|
||||||
|
password: 'sample-password',
|
||||||
|
});
|
||||||
|
|
||||||
|
const token = await User.authenticate(
|
||||||
|
'test-user@automatisch.io',
|
||||||
|
'wrong-password'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(token).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined for non-existing email', async () => {
|
||||||
|
await createUser({
|
||||||
|
email: 'test-user@automatisch.io',
|
||||||
|
password: 'sample-password',
|
||||||
|
});
|
||||||
|
|
||||||
|
const token = await User.authenticate('non-existing-user@automatisch.io');
|
||||||
|
|
||||||
|
expect(token).toBe(undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('login', () => {
|
describe('login', () => {
|
||||||
it('should return true when the given password matches with the user password', async () => {
|
it('should return true when the given password matches with the user password', async () => {
|
||||||
const user = await createUser({ password: 'sample-password' });
|
const user = await createUser({ password: 'sample-password' });
|
||||||
@@ -875,4 +893,637 @@ describe('User model', () => {
|
|||||||
|
|
||||||
vi.useRealTimers();
|
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();
|
||||||
|
|
||||||
|
await user.startTrialPeriod();
|
||||||
|
|
||||||
|
vi.setSystemTime(DateTime.now().plus({ month: 1 }));
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
await 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 () => {
|
||||||
|
await expect(() =>
|
||||||
|
User.registerUser({
|
||||||
|
fullName: 'Sample user',
|
||||||
|
email: 'user@automatisch.io',
|
||||||
|
password: 'sample-password',
|
||||||
|
})
|
||||||
|
).rejects.toThrowError('NotFoundError');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('can', () => {
|
||||||
|
it('should return conditions for the given action and subject of the user', async () => {
|
||||||
|
const userRole = await createRole({ name: 'User' });
|
||||||
|
|
||||||
|
await createPermission({
|
||||||
|
roleId: userRole.id,
|
||||||
|
subject: 'Flow',
|
||||||
|
action: 'read',
|
||||||
|
conditions: ['isCreator'],
|
||||||
|
});
|
||||||
|
|
||||||
|
await createPermission({
|
||||||
|
roleId: userRole.id,
|
||||||
|
subject: 'Connection',
|
||||||
|
action: 'read',
|
||||||
|
conditions: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = await createUser({ roleId: userRole.id });
|
||||||
|
|
||||||
|
const userWithRoleAndPermissions = await user
|
||||||
|
.$query()
|
||||||
|
.withGraphFetched({ role: true, permissions: true });
|
||||||
|
|
||||||
|
expect(userWithRoleAndPermissions.can('read', 'Flow')).toStrictEqual({
|
||||||
|
isCreator: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
userWithRoleAndPermissions.can('read', 'Connection')
|
||||||
|
).toStrictEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return not authorized error when the user is not permitted for the given action and subject', async () => {
|
||||||
|
const userRole = await createRole({ name: 'User' });
|
||||||
|
const user = await createUser({ roleId: userRole.id });
|
||||||
|
|
||||||
|
const userWithRoleAndPermissions = await user
|
||||||
|
.$query()
|
||||||
|
.withGraphFetched({ role: true, permissions: true });
|
||||||
|
|
||||||
|
expect(() => userWithRoleAndPermissions.can('read', 'Flow')).toThrowError(
|
||||||
|
'The user is not authorized!'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lowercaseEmail should lowercase the user email', () => {
|
||||||
|
const user = new User();
|
||||||
|
user.email = 'USER@AUTOMATISCH.IO';
|
||||||
|
|
||||||
|
user.lowercaseEmail();
|
||||||
|
|
||||||
|
expect(user.email).toBe('user@automatisch.io');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createUsageData', () => {
|
||||||
|
it('should create usage data if Automatisch is a cloud installation', async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
|
||||||
|
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(true);
|
||||||
|
|
||||||
|
const user = await createUser({
|
||||||
|
fullName: 'Sample user',
|
||||||
|
email: 'user@automatisch.io',
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.setSystemTime(DateTime.now().plus({ month: 1 }));
|
||||||
|
|
||||||
|
const usageData = await user.createUsageData();
|
||||||
|
const currentUsageData = await user.$relatedQuery('currentUsageData');
|
||||||
|
|
||||||
|
expect(usageData).toStrictEqual(currentUsageData);
|
||||||
|
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not create usage data if Automatisch is not a cloud installation', async () => {
|
||||||
|
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(false);
|
||||||
|
|
||||||
|
const user = await createUser({
|
||||||
|
fullName: 'Sample user',
|
||||||
|
email: 'user@automatisch.io',
|
||||||
|
});
|
||||||
|
|
||||||
|
const usageData = await user.createUsageData();
|
||||||
|
|
||||||
|
expect(usageData).toBe(undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('omitEnterprisePermissionsWithoutValidLicense', () => {
|
||||||
|
it('should return user as-is with valid license', async () => {
|
||||||
|
const userRole = await createRole({ name: 'User' });
|
||||||
|
const user = await createUser({
|
||||||
|
fullName: 'Sample user',
|
||||||
|
email: 'user@automatisch.io',
|
||||||
|
roleId: userRole.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const readFlowPermission = await createPermission({
|
||||||
|
roleId: userRole.id,
|
||||||
|
subject: 'Flow',
|
||||||
|
action: 'read',
|
||||||
|
conditions: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
await createPermission({
|
||||||
|
roleId: userRole.id,
|
||||||
|
subject: 'App',
|
||||||
|
action: 'read',
|
||||||
|
conditions: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
await createPermission({
|
||||||
|
roleId: userRole.id,
|
||||||
|
subject: 'Role',
|
||||||
|
action: 'read',
|
||||||
|
conditions: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
await createPermission({
|
||||||
|
roleId: userRole.id,
|
||||||
|
subject: 'Config',
|
||||||
|
action: 'read',
|
||||||
|
conditions: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
await createPermission({
|
||||||
|
roleId: userRole.id,
|
||||||
|
subject: 'SamlAuthProvider',
|
||||||
|
action: 'read',
|
||||||
|
conditions: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const userWithRoleAndPermissions = await user
|
||||||
|
.$query()
|
||||||
|
.withGraphFetched({ role: true, permissions: true });
|
||||||
|
|
||||||
|
expect(userWithRoleAndPermissions.permissions).toStrictEqual([
|
||||||
|
readFlowPermission,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should omit enterprise permissions without valid license', async () => {
|
||||||
|
vi.spyOn(licenseModule, 'hasValidLicense').mockResolvedValue(false);
|
||||||
|
|
||||||
|
const userRole = await createRole({ name: 'User' });
|
||||||
|
const user = await createUser({
|
||||||
|
fullName: 'Sample user',
|
||||||
|
email: 'user@automatisch.io',
|
||||||
|
roleId: userRole.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const readFlowPermission = await createPermission({
|
||||||
|
roleId: userRole.id,
|
||||||
|
subject: 'Flow',
|
||||||
|
action: 'read',
|
||||||
|
conditions: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
await createPermission({
|
||||||
|
roleId: userRole.id,
|
||||||
|
subject: 'App',
|
||||||
|
action: 'read',
|
||||||
|
conditions: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
await createPermission({
|
||||||
|
roleId: userRole.id,
|
||||||
|
subject: 'Role',
|
||||||
|
action: 'read',
|
||||||
|
conditions: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
await createPermission({
|
||||||
|
roleId: userRole.id,
|
||||||
|
subject: 'Config',
|
||||||
|
action: 'read',
|
||||||
|
conditions: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
await createPermission({
|
||||||
|
roleId: userRole.id,
|
||||||
|
subject: 'SamlAuthProvider',
|
||||||
|
action: 'read',
|
||||||
|
conditions: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const userWithRoleAndPermissions = await user
|
||||||
|
.$query()
|
||||||
|
.withGraphFetched({ role: true, permissions: true });
|
||||||
|
|
||||||
|
expect(userWithRoleAndPermissions.permissions).toStrictEqual([
|
||||||
|
readFlowPermission,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('$beforeInsert', () => {
|
||||||
|
it('should call super.$beforeInsert', async () => {
|
||||||
|
const superBeforeInsertSpy = vi
|
||||||
|
.spyOn(User.prototype, '$beforeInsert')
|
||||||
|
.mockResolvedValue();
|
||||||
|
|
||||||
|
await createUser();
|
||||||
|
|
||||||
|
expect(superBeforeInsertSpy).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should lowercase the user email', async () => {
|
||||||
|
const user = await createUser({
|
||||||
|
fullName: 'Sample user',
|
||||||
|
email: 'USER@AUTOMATISCH.IO',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(user.email).toBe('user@automatisch.io');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate password hash', async () => {
|
||||||
|
const user = await createUser({
|
||||||
|
fullName: 'Sample user',
|
||||||
|
email: 'user@automatisch.io',
|
||||||
|
password: 'sample-password',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(user.password).not.toBe('sample-password');
|
||||||
|
expect(await user.login('sample-password')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should start trial period if Automatisch is a cloud installation', async () => {
|
||||||
|
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(true);
|
||||||
|
|
||||||
|
const startTrialPeriodSpy = vi.spyOn(User.prototype, 'startTrialPeriod');
|
||||||
|
|
||||||
|
await createUser({
|
||||||
|
fullName: 'Sample user',
|
||||||
|
email: 'user@automatisch.io',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(startTrialPeriodSpy).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not start trial period if Automatisch is not a cloud installation', async () => {
|
||||||
|
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(false);
|
||||||
|
|
||||||
|
const startTrialPeriodSpy = vi.spyOn(User.prototype, 'startTrialPeriod');
|
||||||
|
|
||||||
|
await createUser({
|
||||||
|
fullName: 'Sample user',
|
||||||
|
email: 'user@automatisch.io',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(startTrialPeriodSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('$beforeUpdate', () => {
|
||||||
|
it('should call super.$beforeUpdate', async () => {
|
||||||
|
const superBeforeUpdateSpy = vi
|
||||||
|
.spyOn(User.prototype, '$beforeUpdate')
|
||||||
|
.mockResolvedValue();
|
||||||
|
|
||||||
|
const user = await createUser({
|
||||||
|
fullName: 'Sample user',
|
||||||
|
email: 'user@automatisch.io',
|
||||||
|
});
|
||||||
|
|
||||||
|
await user.$query().patch({ fullName: 'Updated user name' });
|
||||||
|
|
||||||
|
expect(superBeforeUpdateSpy).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should lowercase the user email if given', async () => {
|
||||||
|
const user = await createUser({
|
||||||
|
fullName: 'Sample user',
|
||||||
|
email: 'user@automatisch.io',
|
||||||
|
});
|
||||||
|
|
||||||
|
await user.$query().patchAndFetch({ email: 'NEW_EMAIL@AUTOMATISCH.IO' });
|
||||||
|
|
||||||
|
expect(user.email).toBe('new_email@automatisch.io');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate password hash', async () => {
|
||||||
|
const user = await createUser({
|
||||||
|
fullName: 'Sample user',
|
||||||
|
email: 'user@automatisch.io',
|
||||||
|
password: 'sample-password',
|
||||||
|
});
|
||||||
|
|
||||||
|
await user.$query().patchAndFetch({ password: 'new-password' });
|
||||||
|
|
||||||
|
expect(user.password).not.toBe('new-password');
|
||||||
|
expect(await user.login('new-password')).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('$afterInsert', () => {
|
||||||
|
it('should call super.$afterInsert', async () => {
|
||||||
|
const superAfterInsertSpy = vi.spyOn(User.prototype, '$afterInsert');
|
||||||
|
|
||||||
|
await createUser({
|
||||||
|
fullName: 'Sample user',
|
||||||
|
email: 'user@automatisch.io',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(superAfterInsertSpy).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call createUsageData', async () => {
|
||||||
|
const createUsageDataSpy = vi.spyOn(User.prototype, 'createUsageData');
|
||||||
|
|
||||||
|
await createUser({
|
||||||
|
fullName: 'Sample user',
|
||||||
|
email: 'user@automatisch.io',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createUsageDataSpy).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('$afterFind should invoke omitEnterprisePermissionsWithoutValidLicense method', async () => {
|
||||||
|
const omitEnterprisePermissionsWithoutValidLicenseSpy = vi.spyOn(
|
||||||
|
User.prototype,
|
||||||
|
'omitEnterprisePermissionsWithoutValidLicense'
|
||||||
|
);
|
||||||
|
|
||||||
|
await createUser({
|
||||||
|
fullName: 'Sample user',
|
||||||
|
email: 'user@automatisch.io',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
omitEnterprisePermissionsWithoutValidLicenseSpy
|
||||||
|
).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -11,10 +11,6 @@ const redisConnection = {
|
|||||||
|
|
||||||
const actionQueue = new Queue('action', redisConnection);
|
const actionQueue = new Queue('action', redisConnection);
|
||||||
|
|
||||||
process.on('SIGTERM', async () => {
|
|
||||||
await actionQueue.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
actionQueue.on('error', (error) => {
|
actionQueue.on('error', (error) => {
|
||||||
if (error.code === CONNECTION_REFUSED) {
|
if (error.code === CONNECTION_REFUSED) {
|
||||||
logger.error(
|
logger.error(
|
||||||
|
@@ -11,10 +11,6 @@ const redisConnection = {
|
|||||||
|
|
||||||
const deleteUserQueue = new Queue('delete-user', redisConnection);
|
const deleteUserQueue = new Queue('delete-user', redisConnection);
|
||||||
|
|
||||||
process.on('SIGTERM', async () => {
|
|
||||||
await deleteUserQueue.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
deleteUserQueue.on('error', (error) => {
|
deleteUserQueue.on('error', (error) => {
|
||||||
if (error.code === CONNECTION_REFUSED) {
|
if (error.code === CONNECTION_REFUSED) {
|
||||||
logger.error(
|
logger.error(
|
||||||
|
@@ -11,10 +11,6 @@ const redisConnection = {
|
|||||||
|
|
||||||
const emailQueue = new Queue('email', redisConnection);
|
const emailQueue = new Queue('email', redisConnection);
|
||||||
|
|
||||||
process.on('SIGTERM', async () => {
|
|
||||||
await emailQueue.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
emailQueue.on('error', (error) => {
|
emailQueue.on('error', (error) => {
|
||||||
if (error.code === CONNECTION_REFUSED) {
|
if (error.code === CONNECTION_REFUSED) {
|
||||||
logger.error(
|
logger.error(
|
||||||
|
@@ -11,10 +11,6 @@ const redisConnection = {
|
|||||||
|
|
||||||
const flowQueue = new Queue('flow', redisConnection);
|
const flowQueue = new Queue('flow', redisConnection);
|
||||||
|
|
||||||
process.on('SIGTERM', async () => {
|
|
||||||
await flowQueue.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
flowQueue.on('error', (error) => {
|
flowQueue.on('error', (error) => {
|
||||||
if (error.code === CONNECTION_REFUSED) {
|
if (error.code === CONNECTION_REFUSED) {
|
||||||
logger.error(
|
logger.error(
|
||||||
|
21
packages/backend/src/queues/index.js
Normal file
21
packages/backend/src/queues/index.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import appConfig from '../config/app.js';
|
||||||
|
import actionQueue from './action.js';
|
||||||
|
import emailQueue from './email.js';
|
||||||
|
import flowQueue from './flow.js';
|
||||||
|
import triggerQueue from './trigger.js';
|
||||||
|
import deleteUserQueue from './delete-user.ee.js';
|
||||||
|
import removeCancelledSubscriptionsQueue from './remove-cancelled-subscriptions.ee.js';
|
||||||
|
|
||||||
|
const queues = [
|
||||||
|
actionQueue,
|
||||||
|
emailQueue,
|
||||||
|
flowQueue,
|
||||||
|
triggerQueue,
|
||||||
|
deleteUserQueue,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (appConfig.isCloud) {
|
||||||
|
queues.push(removeCancelledSubscriptionsQueue);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default queues;
|
@@ -14,10 +14,6 @@ const removeCancelledSubscriptionsQueue = new Queue(
|
|||||||
redisConnection
|
redisConnection
|
||||||
);
|
);
|
||||||
|
|
||||||
process.on('SIGTERM', async () => {
|
|
||||||
await removeCancelledSubscriptionsQueue.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
removeCancelledSubscriptionsQueue.on('error', (error) => {
|
removeCancelledSubscriptionsQueue.on('error', (error) => {
|
||||||
if (error.code === CONNECTION_REFUSED) {
|
if (error.code === CONNECTION_REFUSED) {
|
||||||
logger.error(
|
logger.error(
|
||||||
|
@@ -11,10 +11,6 @@ const redisConnection = {
|
|||||||
|
|
||||||
const triggerQueue = new Queue('trigger', redisConnection);
|
const triggerQueue = new Queue('trigger', redisConnection);
|
||||||
|
|
||||||
process.on('SIGTERM', async () => {
|
|
||||||
await triggerQueue.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
triggerQueue.on('error', (error) => {
|
triggerQueue.on('error', (error) => {
|
||||||
if (error.code === CONNECTION_REFUSED) {
|
if (error.code === CONNECTION_REFUSED) {
|
||||||
logger.error(
|
logger.error(
|
||||||
|
@@ -26,7 +26,7 @@ const serializers = {
|
|||||||
Permission: permissionSerializer,
|
Permission: permissionSerializer,
|
||||||
AdminSamlAuthProvider: adminSamlAuthProviderSerializer,
|
AdminSamlAuthProvider: adminSamlAuthProviderSerializer,
|
||||||
SamlAuthProvider: samlAuthProviderSerializer,
|
SamlAuthProvider: samlAuthProviderSerializer,
|
||||||
SamlAuthProvidersRoleMapping: samlAuthProviderRoleMappingSerializer,
|
RoleMapping: samlAuthProviderRoleMappingSerializer,
|
||||||
AppAuthClient: appAuthClientSerializer,
|
AppAuthClient: appAuthClientSerializer,
|
||||||
AppConfig: appConfigSerializer,
|
AppConfig: appConfigSerializer,
|
||||||
Flow: flowSerializer,
|
Flow: flowSerializer,
|
||||||
|
@@ -1,20 +1,22 @@
|
|||||||
import * as Sentry from './helpers/sentry.ee.js';
|
import * as Sentry from './helpers/sentry.ee.js';
|
||||||
import appConfig from './config/app.js';
|
import process from 'node:process';
|
||||||
|
|
||||||
Sentry.init();
|
Sentry.init();
|
||||||
|
|
||||||
import './config/orm.js';
|
import './config/orm.js';
|
||||||
import './helpers/check-worker-readiness.js';
|
import './helpers/check-worker-readiness.js';
|
||||||
import './workers/flow.js';
|
import queues from './queues/index.js';
|
||||||
import './workers/trigger.js';
|
import workers from './workers/index.js';
|
||||||
import './workers/action.js';
|
|
||||||
import './workers/email.js';
|
|
||||||
import './workers/delete-user.ee.js';
|
|
||||||
|
|
||||||
if (appConfig.isCloud) {
|
process.on('SIGTERM', async () => {
|
||||||
import('./workers/remove-cancelled-subscriptions.ee.js');
|
for (const queue of queues) {
|
||||||
import('./queues/remove-cancelled-subscriptions.ee.js');
|
await queue.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const worker of workers) {
|
||||||
|
await worker.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
import telemetry from './helpers/telemetry/index.js';
|
import telemetry from './helpers/telemetry/index.js';
|
||||||
|
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
import { Worker } from 'bullmq';
|
import { Worker } from 'bullmq';
|
||||||
import process from 'node:process';
|
|
||||||
|
|
||||||
import * as Sentry from '../helpers/sentry.ee.js';
|
import * as Sentry from '../helpers/sentry.ee.js';
|
||||||
import redisConfig from '../config/redis.js';
|
import redisConfig from '../config/redis.js';
|
||||||
@@ -15,7 +14,7 @@ import delayAsMilliseconds from '../helpers/delay-as-milliseconds.js';
|
|||||||
|
|
||||||
const DEFAULT_DELAY_DURATION = 0;
|
const DEFAULT_DELAY_DURATION = 0;
|
||||||
|
|
||||||
export const worker = new Worker(
|
const actionWorker = new Worker(
|
||||||
'action',
|
'action',
|
||||||
async (job) => {
|
async (job) => {
|
||||||
const { stepId, flowId, executionId, computedParameters, executionStep } =
|
const { stepId, flowId, executionId, computedParameters, executionStep } =
|
||||||
@@ -55,11 +54,11 @@ export const worker = new Worker(
|
|||||||
{ connection: redisConfig }
|
{ connection: redisConfig }
|
||||||
);
|
);
|
||||||
|
|
||||||
worker.on('completed', (job) => {
|
actionWorker.on('completed', (job) => {
|
||||||
logger.info(`JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has started!`);
|
logger.info(`JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has started!`);
|
||||||
});
|
});
|
||||||
|
|
||||||
worker.on('failed', (job, err) => {
|
actionWorker.on('failed', (job, err) => {
|
||||||
const errorMessage = `
|
const errorMessage = `
|
||||||
JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has failed to start with ${err.message}
|
JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has failed to start with ${err.message}
|
||||||
\n ${err.stack}
|
\n ${err.stack}
|
||||||
@@ -74,6 +73,4 @@ worker.on('failed', (job, err) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('SIGTERM', async () => {
|
export default actionWorker;
|
||||||
await worker.close();
|
|
||||||
});
|
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
import { Worker } from 'bullmq';
|
import { Worker } from 'bullmq';
|
||||||
import process from 'node:process';
|
|
||||||
|
|
||||||
import * as Sentry from '../helpers/sentry.ee.js';
|
import * as Sentry from '../helpers/sentry.ee.js';
|
||||||
import redisConfig from '../config/redis.js';
|
import redisConfig from '../config/redis.js';
|
||||||
@@ -8,7 +7,7 @@ import appConfig from '../config/app.js';
|
|||||||
import User from '../models/user.js';
|
import User from '../models/user.js';
|
||||||
import ExecutionStep from '../models/execution-step.js';
|
import ExecutionStep from '../models/execution-step.js';
|
||||||
|
|
||||||
export const worker = new Worker(
|
const deleteUserWorker = new Worker(
|
||||||
'delete-user',
|
'delete-user',
|
||||||
async (job) => {
|
async (job) => {
|
||||||
const { id } = job.data;
|
const { id } = job.data;
|
||||||
@@ -46,13 +45,13 @@ export const worker = new Worker(
|
|||||||
{ connection: redisConfig }
|
{ connection: redisConfig }
|
||||||
);
|
);
|
||||||
|
|
||||||
worker.on('completed', (job) => {
|
deleteUserWorker.on('completed', (job) => {
|
||||||
logger.info(
|
logger.info(
|
||||||
`JOB ID: ${job.id} - The user with the ID of '${job.data.id}' has been deleted!`
|
`JOB ID: ${job.id} - The user with the ID of '${job.data.id}' has been deleted!`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
worker.on('failed', (job, err) => {
|
deleteUserWorker.on('failed', (job, err) => {
|
||||||
const errorMessage = `
|
const errorMessage = `
|
||||||
JOB ID: ${job.id} - The user with the ID of '${job.data.id}' has failed to be deleted! ${err.message}
|
JOB ID: ${job.id} - The user with the ID of '${job.data.id}' has failed to be deleted! ${err.message}
|
||||||
\n ${err.stack}
|
\n ${err.stack}
|
||||||
@@ -67,6 +66,4 @@ worker.on('failed', (job, err) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('SIGTERM', async () => {
|
export default deleteUserWorker;
|
||||||
await worker.close();
|
|
||||||
});
|
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
import { Worker } from 'bullmq';
|
import { Worker } from 'bullmq';
|
||||||
import process from 'node:process';
|
|
||||||
|
|
||||||
import * as Sentry from '../helpers/sentry.ee.js';
|
import * as Sentry from '../helpers/sentry.ee.js';
|
||||||
import redisConfig from '../config/redis.js';
|
import redisConfig from '../config/redis.js';
|
||||||
@@ -16,7 +15,7 @@ const isAutomatischEmail = (email) => {
|
|||||||
return email.endsWith('@automatisch.io');
|
return email.endsWith('@automatisch.io');
|
||||||
};
|
};
|
||||||
|
|
||||||
export const worker = new Worker(
|
const emailWorker = new Worker(
|
||||||
'email',
|
'email',
|
||||||
async (job) => {
|
async (job) => {
|
||||||
const { email, subject, template, params } = job.data;
|
const { email, subject, template, params } = job.data;
|
||||||
@@ -39,13 +38,13 @@ export const worker = new Worker(
|
|||||||
{ connection: redisConfig }
|
{ connection: redisConfig }
|
||||||
);
|
);
|
||||||
|
|
||||||
worker.on('completed', (job) => {
|
emailWorker.on('completed', (job) => {
|
||||||
logger.info(
|
logger.info(
|
||||||
`JOB ID: ${job.id} - ${job.data.subject} email sent to ${job.data.email}!`
|
`JOB ID: ${job.id} - ${job.data.subject} email sent to ${job.data.email}!`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
worker.on('failed', (job, err) => {
|
emailWorker.on('failed', (job, err) => {
|
||||||
const errorMessage = `
|
const errorMessage = `
|
||||||
JOB ID: ${job.id} - ${job.data.subject} email to ${job.data.email} has failed to send with ${err.message}
|
JOB ID: ${job.id} - ${job.data.subject} email to ${job.data.email} has failed to send with ${err.message}
|
||||||
\n ${err.stack}
|
\n ${err.stack}
|
||||||
@@ -60,6 +59,4 @@ worker.on('failed', (job, err) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('SIGTERM', async () => {
|
export default emailWorker;
|
||||||
await worker.close();
|
|
||||||
});
|
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
import { Worker } from 'bullmq';
|
import { Worker } from 'bullmq';
|
||||||
import process from 'node:process';
|
|
||||||
|
|
||||||
import * as Sentry from '../helpers/sentry.ee.js';
|
import * as Sentry from '../helpers/sentry.ee.js';
|
||||||
import redisConfig from '../config/redis.js';
|
import redisConfig from '../config/redis.js';
|
||||||
@@ -13,7 +12,7 @@ import {
|
|||||||
REMOVE_AFTER_7_DAYS_OR_50_JOBS,
|
REMOVE_AFTER_7_DAYS_OR_50_JOBS,
|
||||||
} from '../helpers/remove-job-configuration.js';
|
} from '../helpers/remove-job-configuration.js';
|
||||||
|
|
||||||
export const worker = new Worker(
|
const flowWorker = new Worker(
|
||||||
'flow',
|
'flow',
|
||||||
async (job) => {
|
async (job) => {
|
||||||
const { flowId } = job.data;
|
const { flowId } = job.data;
|
||||||
@@ -64,11 +63,11 @@ export const worker = new Worker(
|
|||||||
{ connection: redisConfig }
|
{ connection: redisConfig }
|
||||||
);
|
);
|
||||||
|
|
||||||
worker.on('completed', (job) => {
|
flowWorker.on('completed', (job) => {
|
||||||
logger.info(`JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has started!`);
|
logger.info(`JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has started!`);
|
||||||
});
|
});
|
||||||
|
|
||||||
worker.on('failed', async (job, err) => {
|
flowWorker.on('failed', async (job, err) => {
|
||||||
const errorMessage = `
|
const errorMessage = `
|
||||||
JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has failed to start with ${err.message}
|
JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has failed to start with ${err.message}
|
||||||
\n ${err.stack}
|
\n ${err.stack}
|
||||||
@@ -95,6 +94,4 @@ worker.on('failed', async (job, err) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('SIGTERM', async () => {
|
export default flowWorker;
|
||||||
await worker.close();
|
|
||||||
});
|
|
||||||
|
21
packages/backend/src/workers/index.js
Normal file
21
packages/backend/src/workers/index.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import appConfig from '../config/app.js';
|
||||||
|
import actionWorker from './action.js';
|
||||||
|
import emailWorker from './email.js';
|
||||||
|
import flowWorker from './flow.js';
|
||||||
|
import triggerWorker from './trigger.js';
|
||||||
|
import deleteUserWorker from './delete-user.ee.js';
|
||||||
|
import removeCancelledSubscriptionsWorker from './remove-cancelled-subscriptions.ee.js';
|
||||||
|
|
||||||
|
const workers = [
|
||||||
|
actionWorker,
|
||||||
|
emailWorker,
|
||||||
|
flowWorker,
|
||||||
|
triggerWorker,
|
||||||
|
deleteUserWorker,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (appConfig.isCloud) {
|
||||||
|
workers.push(removeCancelledSubscriptionsWorker);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default workers;
|
@@ -1,12 +1,11 @@
|
|||||||
import { Worker } from 'bullmq';
|
import { Worker } from 'bullmq';
|
||||||
import process from 'node:process';
|
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import * as Sentry from '../helpers/sentry.ee.js';
|
import * as Sentry from '../helpers/sentry.ee.js';
|
||||||
import redisConfig from '../config/redis.js';
|
import redisConfig from '../config/redis.js';
|
||||||
import logger from '../helpers/logger.js';
|
import logger from '../helpers/logger.js';
|
||||||
import Subscription from '../models/subscription.ee.js';
|
import Subscription from '../models/subscription.ee.js';
|
||||||
|
|
||||||
export const worker = new Worker(
|
const removeCancelledSubscriptionsWorker = new Worker(
|
||||||
'remove-cancelled-subscriptions',
|
'remove-cancelled-subscriptions',
|
||||||
async () => {
|
async () => {
|
||||||
await Subscription.query()
|
await Subscription.query()
|
||||||
@@ -23,13 +22,13 @@ export const worker = new Worker(
|
|||||||
{ connection: redisConfig }
|
{ connection: redisConfig }
|
||||||
);
|
);
|
||||||
|
|
||||||
worker.on('completed', (job) => {
|
removeCancelledSubscriptionsWorker.on('completed', (job) => {
|
||||||
logger.info(
|
logger.info(
|
||||||
`JOB ID: ${job.id} - The cancelled subscriptions have been removed!`
|
`JOB ID: ${job.id} - The cancelled subscriptions have been removed!`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
worker.on('failed', (job, err) => {
|
removeCancelledSubscriptionsWorker.on('failed', (job, err) => {
|
||||||
const errorMessage = `
|
const errorMessage = `
|
||||||
JOB ID: ${job.id} - ERROR: The cancelled subscriptions can not be removed! ${err.message}
|
JOB ID: ${job.id} - ERROR: The cancelled subscriptions can not be removed! ${err.message}
|
||||||
\n ${err.stack}
|
\n ${err.stack}
|
||||||
@@ -42,6 +41,4 @@ worker.on('failed', (job, err) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('SIGTERM', async () => {
|
export default removeCancelledSubscriptionsWorker;
|
||||||
await worker.close();
|
|
||||||
});
|
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
import { Worker } from 'bullmq';
|
import { Worker } from 'bullmq';
|
||||||
import process from 'node:process';
|
|
||||||
|
|
||||||
import * as Sentry from '../helpers/sentry.ee.js';
|
import * as Sentry from '../helpers/sentry.ee.js';
|
||||||
import redisConfig from '../config/redis.js';
|
import redisConfig from '../config/redis.js';
|
||||||
@@ -12,7 +11,7 @@ import {
|
|||||||
REMOVE_AFTER_7_DAYS_OR_50_JOBS,
|
REMOVE_AFTER_7_DAYS_OR_50_JOBS,
|
||||||
} from '../helpers/remove-job-configuration.js';
|
} from '../helpers/remove-job-configuration.js';
|
||||||
|
|
||||||
export const worker = new Worker(
|
const triggerWorker = new Worker(
|
||||||
'trigger',
|
'trigger',
|
||||||
async (job) => {
|
async (job) => {
|
||||||
const { flowId, executionId, stepId, executionStep } = await processTrigger(
|
const { flowId, executionId, stepId, executionStep } = await processTrigger(
|
||||||
@@ -41,11 +40,11 @@ export const worker = new Worker(
|
|||||||
{ connection: redisConfig }
|
{ connection: redisConfig }
|
||||||
);
|
);
|
||||||
|
|
||||||
worker.on('completed', (job) => {
|
triggerWorker.on('completed', (job) => {
|
||||||
logger.info(`JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has started!`);
|
logger.info(`JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has started!`);
|
||||||
});
|
});
|
||||||
|
|
||||||
worker.on('failed', (job, err) => {
|
triggerWorker.on('failed', (job, err) => {
|
||||||
const errorMessage = `
|
const errorMessage = `
|
||||||
JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has failed to start with ${err.message}
|
JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has failed to start with ${err.message}
|
||||||
\n ${err.stack}
|
\n ${err.stack}
|
||||||
@@ -60,6 +59,4 @@ worker.on('failed', (job, err) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('SIGTERM', async () => {
|
export default triggerWorker;
|
||||||
await worker.close();
|
|
||||||
});
|
|
||||||
|
@@ -1,16 +1,15 @@
|
|||||||
|
import { faker } from '@faker-js/faker';
|
||||||
import { createRole } from './role.js';
|
import { createRole } from './role.js';
|
||||||
|
import RoleMapping from '../../src/models/role-mapping.ee.js';
|
||||||
import { createSamlAuthProvider } from './saml-auth-provider.ee.js';
|
import { createSamlAuthProvider } from './saml-auth-provider.ee.js';
|
||||||
import SamlAuthProviderRoleMapping from '../../src/models/saml-auth-providers-role-mapping.ee.js';
|
|
||||||
|
|
||||||
export const createRoleMapping = async (params = {}) => {
|
export const createRoleMapping = async (params = {}) => {
|
||||||
params.roleId = params?.roleId || (await createRole()).id;
|
params.roleId = params.roleId || (await createRole()).id;
|
||||||
params.samlAuthProviderId =
|
params.samlAuthProviderId =
|
||||||
params?.samlAuthProviderId || (await createSamlAuthProvider()).id;
|
params.samlAuthProviderId || (await createSamlAuthProvider()).id;
|
||||||
|
params.remoteRoleName = params.remoteRoleName || faker.person.jobType();
|
||||||
|
|
||||||
params.remoteRoleName = params?.remoteRoleName || 'User';
|
const roleMapping = await RoleMapping.query().insertAndFetch(params);
|
||||||
|
|
||||||
const samlAuthProviderRoleMapping =
|
return roleMapping;
|
||||||
await SamlAuthProviderRoleMapping.query().insertAndFetch(params);
|
|
||||||
|
|
||||||
return samlAuthProviderRoleMapping;
|
|
||||||
};
|
};
|
||||||
|
@@ -1,16 +0,0 @@
|
|||||||
import { faker } from '@faker-js/faker';
|
|
||||||
import { createRole } from './role.js';
|
|
||||||
import SamlAuthProvidersRoleMapping from '../../src/models/saml-auth-providers-role-mapping.ee.js';
|
|
||||||
import { createSamlAuthProvider } from './saml-auth-provider.ee.js';
|
|
||||||
|
|
||||||
export const createSamlAuthProvidersRoleMapping = async (params = {}) => {
|
|
||||||
params.roleId = params.roleId || (await createRole()).id;
|
|
||||||
params.samlAuthProviderId =
|
|
||||||
params.samlAuthProviderId || (await createSamlAuthProvider()).id;
|
|
||||||
params.remoteRoleName = params.remoteRoleName || faker.person.jobType();
|
|
||||||
|
|
||||||
const samlAuthProvider =
|
|
||||||
await SamlAuthProvidersRoleMapping.query().insertAndFetch(params);
|
|
||||||
|
|
||||||
return samlAuthProvider;
|
|
||||||
};
|
|
@@ -15,7 +15,7 @@ const getRoleMappingsMock = async (roleMappings) => {
|
|||||||
currentPage: null,
|
currentPage: null,
|
||||||
isArray: true,
|
isArray: true,
|
||||||
totalPages: null,
|
totalPages: null,
|
||||||
type: 'SamlAuthProvidersRoleMapping',
|
type: 'RoleMapping',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@@ -15,7 +15,7 @@ const createRoleMappingsMock = async (roleMappings) => {
|
|||||||
currentPage: null,
|
currentPage: null,
|
||||||
isArray: true,
|
isArray: true,
|
||||||
totalPages: null,
|
totalPages: null,
|
||||||
type: 'SamlAuthProvidersRoleMapping',
|
type: 'RoleMapping',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
const { expect } = require('@playwright/test');
|
||||||
|
|
||||||
const { faker } = require('@faker-js/faker');
|
const { faker } = require('@faker-js/faker');
|
||||||
const { AuthenticatedPage } = require('../authenticated-page');
|
const { AuthenticatedPage } = require('../authenticated-page');
|
||||||
|
|
||||||
@@ -11,11 +13,17 @@ export class AdminCreateUserPage extends AuthenticatedPage {
|
|||||||
super(page);
|
super(page);
|
||||||
this.fullNameInput = page.getByTestId('full-name-input');
|
this.fullNameInput = page.getByTestId('full-name-input');
|
||||||
this.emailInput = page.getByTestId('email-input');
|
this.emailInput = page.getByTestId('email-input');
|
||||||
this.roleInput = page.getByTestId('role.id-autocomplete');
|
this.roleInput = page.getByTestId('roleId-autocomplete');
|
||||||
this.createButton = page.getByTestId('create-button');
|
this.createButton = page.getByTestId('create-button');
|
||||||
this.pageTitle = page.getByTestId('create-user-title');
|
this.pageTitle = page.getByTestId('create-user-title');
|
||||||
this.invitationEmailInfoAlert = page.getByTestId('invitation-email-info-alert');
|
this.invitationEmailInfoAlert = page.getByTestId(
|
||||||
this.acceptInvitationLink = page.getByTestId('invitation-email-info-alert').getByRole('link');
|
'invitation-email-info-alert'
|
||||||
|
);
|
||||||
|
this.acceptInvitationLink = page
|
||||||
|
.getByTestId('invitation-email-info-alert')
|
||||||
|
.getByRole('link');
|
||||||
|
this.createUserSuccessAlert = page.getByTestId('create-user-success-alert');
|
||||||
|
this.fieldError = page.locator('p[id$="-helper-text"]');
|
||||||
}
|
}
|
||||||
|
|
||||||
seed(seed) {
|
seed(seed) {
|
||||||
@@ -28,4 +36,8 @@ export class AdminCreateUserPage extends AuthenticatedPage {
|
|||||||
email: faker.internet.email().toLowerCase(),
|
email: faker.internet.email().toLowerCase(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async expectCreateUserSuccessAlertToBeVisible() {
|
||||||
|
await expect(this.createUserSuccessAlert).toBeVisible();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -35,9 +35,8 @@ test.describe('Role management page', () => {
|
|||||||
await adminCreateRolePage.closeSnackbar();
|
await adminCreateRolePage.closeSnackbar();
|
||||||
});
|
});
|
||||||
|
|
||||||
let roleRow = await test.step(
|
let roleRow =
|
||||||
'Make sure role data is correct',
|
await test.step('Make sure role data is correct', async () => {
|
||||||
async () => {
|
|
||||||
const roleRow = await adminRolesPage.getRoleRowByName(
|
const roleRow = await adminRolesPage.getRoleRowByName(
|
||||||
'Create Edit Test'
|
'Create Edit Test'
|
||||||
);
|
);
|
||||||
@@ -48,8 +47,7 @@ test.describe('Role management page', () => {
|
|||||||
await expect(roleData.canEdit).toBe(true);
|
await expect(roleData.canEdit).toBe(true);
|
||||||
await expect(roleData.canDelete).toBe(true);
|
await expect(roleData.canDelete).toBe(true);
|
||||||
return roleRow;
|
return roleRow;
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
await test.step('Edit the role', async () => {
|
await test.step('Edit the role', async () => {
|
||||||
await adminRolesPage.clickEditRole(roleRow);
|
await adminRolesPage.clickEditRole(roleRow);
|
||||||
@@ -67,9 +65,8 @@ test.describe('Role management page', () => {
|
|||||||
await adminEditRolePage.closeSnackbar();
|
await adminEditRolePage.closeSnackbar();
|
||||||
});
|
});
|
||||||
|
|
||||||
roleRow = await test.step(
|
roleRow =
|
||||||
'Make sure changes reflected on roles page',
|
await test.step('Make sure changes reflected on roles page', async () => {
|
||||||
async () => {
|
|
||||||
await adminRolesPage.isMounted();
|
await adminRolesPage.isMounted();
|
||||||
const roleRow = await adminRolesPage.getRoleRowByName(
|
const roleRow = await adminRolesPage.getRoleRowByName(
|
||||||
'Create Update Test'
|
'Create Update Test'
|
||||||
@@ -81,8 +78,7 @@ test.describe('Role management page', () => {
|
|||||||
await expect(roleData.canEdit).toBe(true);
|
await expect(roleData.canEdit).toBe(true);
|
||||||
await expect(roleData.canDelete).toBe(true);
|
await expect(roleData.canDelete).toBe(true);
|
||||||
return roleRow;
|
return roleRow;
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
await test.step('Delete the role', async () => {
|
await test.step('Delete the role', async () => {
|
||||||
await adminRolesPage.clickDeleteRole(roleRow);
|
await adminRolesPage.clickDeleteRole(roleRow);
|
||||||
@@ -184,9 +180,7 @@ test.describe('Role management page', () => {
|
|||||||
await expect(snackbar.variant).toBe('success');
|
await expect(snackbar.variant).toBe('success');
|
||||||
await adminCreateRolePage.closeSnackbar();
|
await adminCreateRolePage.closeSnackbar();
|
||||||
});
|
});
|
||||||
await test.step(
|
await test.step('Create a new user with the "Delete Role" role', async () => {
|
||||||
'Create a new user with the "Delete Role" role',
|
|
||||||
async () => {
|
|
||||||
await adminUsersPage.navigateTo();
|
await adminUsersPage.navigateTo();
|
||||||
await adminUsersPage.createUserButton.click();
|
await adminUsersPage.createUserButton.click();
|
||||||
await adminCreateUserPage.fullNameInput.fill('User Role Test');
|
await adminCreateUserPage.fullNameInput.fill('User Role Test');
|
||||||
@@ -198,22 +192,13 @@ test.describe('Role management page', () => {
|
|||||||
.getByRole('option', { name: 'Delete Role', exact: true })
|
.getByRole('option', { name: 'Delete Role', exact: true })
|
||||||
.click();
|
.click();
|
||||||
await adminCreateUserPage.createButton.click();
|
await adminCreateUserPage.createButton.click();
|
||||||
await adminCreateUserPage.snackbar.waitFor({
|
|
||||||
state: 'attached',
|
|
||||||
});
|
|
||||||
await adminCreateUserPage.invitationEmailInfoAlert.waitFor({
|
await adminCreateUserPage.invitationEmailInfoAlert.waitFor({
|
||||||
state: 'attached',
|
state: 'attached',
|
||||||
});
|
});
|
||||||
const snackbar = await adminUsersPage.getSnackbarData(
|
await adminCreateUserPage.expectCreateUserSuccessAlertToBeVisible();
|
||||||
'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 adminUsersPage.closeSnackbar();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
await test.step(
|
|
||||||
'Try to delete "Delete Role" role when new user has it',
|
|
||||||
async () => {
|
|
||||||
await adminRolesPage.navigateTo();
|
await adminRolesPage.navigateTo();
|
||||||
const row = await adminRolesPage.getRoleRowByName('Delete Role');
|
const row = await adminRolesPage.getRoleRowByName('Delete Role');
|
||||||
const modal = await adminRolesPage.clickDeleteRole(row);
|
const modal = await adminRolesPage.clickDeleteRole(row);
|
||||||
@@ -221,12 +206,13 @@ test.describe('Role management page', () => {
|
|||||||
await adminRolesPage.snackbar.waitFor({
|
await adminRolesPage.snackbar.waitFor({
|
||||||
state: 'attached',
|
state: 'attached',
|
||||||
});
|
});
|
||||||
const snackbar = await adminRolesPage.getSnackbarData('snackbar-delete-role-error');
|
const snackbar = await adminRolesPage.getSnackbarData(
|
||||||
|
'snackbar-delete-role-error'
|
||||||
|
);
|
||||||
await expect(snackbar.variant).toBe('error');
|
await expect(snackbar.variant).toBe('error');
|
||||||
await adminRolesPage.closeSnackbar();
|
await adminRolesPage.closeSnackbar();
|
||||||
await modal.close();
|
await modal.close();
|
||||||
}
|
});
|
||||||
);
|
|
||||||
await test.step('Change the role the user has', async () => {
|
await test.step('Change the role the user has', async () => {
|
||||||
await adminUsersPage.navigateTo();
|
await adminUsersPage.navigateTo();
|
||||||
await adminUsersPage.usersLoader.waitFor({
|
await adminUsersPage.usersLoader.waitFor({
|
||||||
@@ -301,24 +287,16 @@ test.describe('Role management page', () => {
|
|||||||
.getByRole('option', { name: 'Cannot Delete Role' })
|
.getByRole('option', { name: 'Cannot Delete Role' })
|
||||||
.click();
|
.click();
|
||||||
await adminCreateUserPage.createButton.click();
|
await adminCreateUserPage.createButton.click();
|
||||||
await adminCreateUserPage.snackbar.waitFor({
|
|
||||||
state: 'attached',
|
|
||||||
});
|
|
||||||
await adminCreateUserPage.invitationEmailInfoAlert.waitFor({
|
await adminCreateUserPage.invitationEmailInfoAlert.waitFor({
|
||||||
state: 'attached',
|
state: 'attached',
|
||||||
});
|
});
|
||||||
const snackbar = await adminCreateUserPage.getSnackbarData(
|
await adminCreateUserPage.expectCreateUserSuccessAlertToBeVisible();
|
||||||
'snackbar-create-user-success'
|
|
||||||
);
|
|
||||||
await expect(snackbar.variant).toBe('success');
|
|
||||||
await adminCreateUserPage.closeSnackbar();
|
|
||||||
});
|
});
|
||||||
await test.step('Delete this user', async () => {
|
await test.step('Delete this user', async () => {
|
||||||
await adminUsersPage.navigateTo();
|
await adminUsersPage.navigateTo();
|
||||||
const row = await adminUsersPage.findUserPageWithEmail(
|
const row = await adminUsersPage.findUserPageWithEmail(
|
||||||
'user-delete-role-test@automatisch.io'
|
'user-delete-role-test@automatisch.io'
|
||||||
);
|
);
|
||||||
// await test.waitForTimeout(10000);
|
|
||||||
const modal = await adminUsersPage.clickDeleteUser(row);
|
const modal = await adminUsersPage.clickDeleteUser(row);
|
||||||
await modal.deleteButton.click();
|
await modal.deleteButton.click();
|
||||||
await adminUsersPage.snackbar.waitFor({
|
await adminUsersPage.snackbar.waitFor({
|
||||||
@@ -385,17 +363,10 @@ test('Accessibility of role management page', async ({
|
|||||||
.getByRole('option', { name: 'Basic Test' })
|
.getByRole('option', { name: 'Basic Test' })
|
||||||
.click();
|
.click();
|
||||||
await adminCreateUserPage.createButton.click();
|
await adminCreateUserPage.createButton.click();
|
||||||
await adminCreateUserPage.snackbar.waitFor({
|
|
||||||
state: 'attached',
|
|
||||||
});
|
|
||||||
await adminCreateUserPage.invitationEmailInfoAlert.waitFor({
|
await adminCreateUserPage.invitationEmailInfoAlert.waitFor({
|
||||||
state: 'attached',
|
state: 'attached',
|
||||||
});
|
});
|
||||||
const snackbar = await adminCreateUserPage.getSnackbarData(
|
await adminCreateUserPage.expectCreateUserSuccessAlertToBeVisible();
|
||||||
'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 () => {
|
await test.step('Logout and login to the basic role user', async () => {
|
||||||
@@ -409,22 +380,16 @@ test('Accessibility of role management page', async ({
|
|||||||
await page.getByTestId('logout-item').click();
|
await page.getByTestId('logout-item').click();
|
||||||
|
|
||||||
const acceptInvitationPage = new AcceptInvitation(page);
|
const acceptInvitationPage = new AcceptInvitation(page);
|
||||||
|
|
||||||
await acceptInvitationPage.open(acceptInvitatonToken);
|
await acceptInvitationPage.open(acceptInvitatonToken);
|
||||||
|
|
||||||
await acceptInvitationPage.acceptInvitation('sample');
|
await acceptInvitationPage.acceptInvitation('sample');
|
||||||
|
|
||||||
const loginPage = new LoginPage(page);
|
const loginPage = new LoginPage(page);
|
||||||
|
|
||||||
// await loginPage.isMounted();
|
|
||||||
await loginPage.login('basic-role-test@automatisch.io', 'sample');
|
await loginPage.login('basic-role-test@automatisch.io', 'sample');
|
||||||
await expect(loginPage.loginButton).not.toBeVisible();
|
await expect(loginPage.loginButton).not.toBeVisible();
|
||||||
await expect(page).toHaveURL('/flows');
|
await expect(page).toHaveURL('/flows');
|
||||||
});
|
});
|
||||||
|
|
||||||
await test.step(
|
await test.step('Navigate to the admin settings page and make sure it is blank', async () => {
|
||||||
'Navigate to the admin settings page and make sure it is blank',
|
|
||||||
async () => {
|
|
||||||
const pageUrl = new URL(page.url());
|
const pageUrl = new URL(page.url());
|
||||||
const url = `${pageUrl.origin}/admin-settings/users`;
|
const url = `${pageUrl.origin}/admin-settings/users`;
|
||||||
await page.goto(url);
|
await page.goto(url);
|
||||||
@@ -443,8 +408,7 @@ test('Accessibility of role management page', async ({
|
|||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
await expect(isUnmounted).toBe(true);
|
await expect(isUnmounted).toBe(true);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
await test.step('Log back into the admin account', async () => {
|
await test.step('Log back into the admin account', async () => {
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
|
@@ -5,15 +5,16 @@ const { test, expect } = require('../../fixtures/index');
|
|||||||
* otherwise tests will fail since users are only *soft*-deleted
|
* otherwise tests will fail since users are only *soft*-deleted
|
||||||
*/
|
*/
|
||||||
test.describe('User management page', () => {
|
test.describe('User management page', () => {
|
||||||
|
|
||||||
test.beforeEach(async ({ adminUsersPage }) => {
|
test.beforeEach(async ({ adminUsersPage }) => {
|
||||||
await adminUsersPage.navigateTo();
|
await adminUsersPage.navigateTo();
|
||||||
await adminUsersPage.closeSnackbar();
|
await adminUsersPage.closeSnackbar();
|
||||||
});
|
});
|
||||||
|
|
||||||
test(
|
test('User creation and deletion process', async ({
|
||||||
'User creation and deletion process',
|
adminCreateUserPage,
|
||||||
async ({ adminCreateUserPage, adminEditUserPage, adminUsersPage }) => {
|
adminEditUserPage,
|
||||||
|
adminUsersPage,
|
||||||
|
}) => {
|
||||||
adminCreateUserPage.seed(9000);
|
adminCreateUserPage.seed(9000);
|
||||||
const user = adminCreateUserPage.generateUser();
|
const user = adminCreateUserPage.generateUser();
|
||||||
await adminUsersPage.usersLoader.waitFor({
|
await adminUsersPage.usersLoader.waitFor({
|
||||||
@@ -21,45 +22,33 @@ test.describe('User management page', () => {
|
|||||||
because visibility: hidden is used as part of the state transition in
|
because visibility: hidden is used as part of the state transition in
|
||||||
notistack, see
|
notistack, see
|
||||||
https://github.com/iamhosseindhv/notistack/blob/122f47057eb7ce5a1abfe923316cf8475303e99a/src/transitions/Collapse/Collapse.tsx#L110
|
https://github.com/iamhosseindhv/notistack/blob/122f47057eb7ce5a1abfe923316cf8475303e99a/src/transitions/Collapse/Collapse.tsx#L110
|
||||||
*/
|
*/,
|
||||||
});
|
});
|
||||||
await test.step(
|
await test.step('Create a user', async () => {
|
||||||
'Create a user',
|
|
||||||
async () => {
|
|
||||||
await adminUsersPage.createUserButton.click();
|
await adminUsersPage.createUserButton.click();
|
||||||
await adminCreateUserPage.fullNameInput.fill(user.fullName);
|
await adminCreateUserPage.fullNameInput.fill(user.fullName);
|
||||||
await adminCreateUserPage.emailInput.fill(user.email);
|
await adminCreateUserPage.emailInput.fill(user.email);
|
||||||
await adminCreateUserPage.roleInput.click();
|
await adminCreateUserPage.roleInput.click();
|
||||||
await adminCreateUserPage.page.getByRole(
|
await adminCreateUserPage.page
|
||||||
'option', { name: 'Admin' }
|
.getByRole('option', { name: 'Admin' })
|
||||||
).click();
|
.click();
|
||||||
await adminCreateUserPage.createButton.click();
|
await adminCreateUserPage.createButton.click();
|
||||||
await adminCreateUserPage.invitationEmailInfoAlert.waitFor({
|
await adminCreateUserPage.invitationEmailInfoAlert.waitFor({
|
||||||
state: 'attached'
|
state: 'attached',
|
||||||
});
|
});
|
||||||
|
|
||||||
const snackbar = await adminUsersPage.getSnackbarData(
|
await adminCreateUserPage.expectCreateUserSuccessAlertToBeVisible();
|
||||||
'snackbar-create-user-success'
|
|
||||||
);
|
|
||||||
await expect(snackbar.variant).toBe('success');
|
|
||||||
await adminUsersPage.navigateTo();
|
await adminUsersPage.navigateTo();
|
||||||
await adminUsersPage.closeSnackbar();
|
});
|
||||||
}
|
await test.step('Check the user exists with the expected properties', async () => {
|
||||||
);
|
|
||||||
await test.step(
|
|
||||||
'Check the user exists with the expected properties',
|
|
||||||
async () => {
|
|
||||||
await adminUsersPage.findUserPageWithEmail(user.email);
|
await adminUsersPage.findUserPageWithEmail(user.email);
|
||||||
const userRow = await adminUsersPage.getUserRowByEmail(user.email);
|
const userRow = await adminUsersPage.getUserRowByEmail(user.email);
|
||||||
const data = await adminUsersPage.getRowData(userRow);
|
const data = await adminUsersPage.getRowData(userRow);
|
||||||
await expect(data.email).toBe(user.email);
|
await expect(data.email).toBe(user.email);
|
||||||
await expect(data.fullName).toBe(user.fullName);
|
await expect(data.fullName).toBe(user.fullName);
|
||||||
await expect(data.role).toBe('Admin');
|
await expect(data.role).toBe('Admin');
|
||||||
}
|
});
|
||||||
);
|
await test.step('Edit user info and make sure the edit works correctly', async () => {
|
||||||
await test.step(
|
|
||||||
'Edit user info and make sure the edit works correctly',
|
|
||||||
async () => {
|
|
||||||
await adminUsersPage.findUserPageWithEmail(user.email);
|
await adminUsersPage.findUserPageWithEmail(user.email);
|
||||||
|
|
||||||
let userRow = await adminUsersPage.getUserRowByEmail(user.email);
|
let userRow = await adminUsersPage.getUserRowByEmail(user.email);
|
||||||
@@ -79,11 +68,8 @@ test.describe('User management page', () => {
|
|||||||
userRow = await adminUsersPage.getUserRowByEmail(user.email);
|
userRow = await adminUsersPage.getUserRowByEmail(user.email);
|
||||||
const rowData = await adminUsersPage.getRowData(userRow);
|
const rowData = await adminUsersPage.getRowData(userRow);
|
||||||
await expect(rowData.fullName).toBe(newUserInfo.fullName);
|
await expect(rowData.fullName).toBe(newUserInfo.fullName);
|
||||||
}
|
});
|
||||||
);
|
await test.step('Delete user and check the page confirms this deletion', async () => {
|
||||||
await test.step(
|
|
||||||
'Delete user and check the page confirms this deletion',
|
|
||||||
async () => {
|
|
||||||
await adminUsersPage.findUserPageWithEmail(user.email);
|
await adminUsersPage.findUserPageWithEmail(user.email);
|
||||||
const userRow = await adminUsersPage.getUserRowByEmail(user.email);
|
const userRow = await adminUsersPage.getUserRowByEmail(user.email);
|
||||||
await adminUsersPage.clickDeleteUser(userRow);
|
await adminUsersPage.clickDeleteUser(userRow);
|
||||||
@@ -96,39 +82,30 @@ test.describe('User management page', () => {
|
|||||||
await expect(snackbar.variant).toBe('success');
|
await expect(snackbar.variant).toBe('success');
|
||||||
await adminUsersPage.closeSnackbar();
|
await adminUsersPage.closeSnackbar();
|
||||||
await expect(userRow).not.toBeVisible(false);
|
await expect(userRow).not.toBeVisible(false);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test(
|
test('Creating a user which has been deleted', async ({
|
||||||
'Creating a user which has been deleted',
|
adminCreateUserPage,
|
||||||
async ({ adminCreateUserPage, adminUsersPage }) => {
|
adminUsersPage,
|
||||||
|
}) => {
|
||||||
adminCreateUserPage.seed(9100);
|
adminCreateUserPage.seed(9100);
|
||||||
const testUser = adminCreateUserPage.generateUser();
|
const testUser = adminCreateUserPage.generateUser();
|
||||||
|
|
||||||
await test.step(
|
await test.step('Create the test user', async () => {
|
||||||
'Create the test user',
|
|
||||||
async () => {
|
|
||||||
await adminUsersPage.navigateTo();
|
await adminUsersPage.navigateTo();
|
||||||
await adminUsersPage.createUserButton.click();
|
await adminUsersPage.createUserButton.click();
|
||||||
await adminCreateUserPage.fullNameInput.fill(testUser.fullName);
|
await adminCreateUserPage.fullNameInput.fill(testUser.fullName);
|
||||||
await adminCreateUserPage.emailInput.fill(testUser.email);
|
await adminCreateUserPage.emailInput.fill(testUser.email);
|
||||||
await adminCreateUserPage.roleInput.click();
|
await adminCreateUserPage.roleInput.click();
|
||||||
await adminCreateUserPage.page.getByRole(
|
await adminCreateUserPage.page
|
||||||
'option', { name: 'Admin' }
|
.getByRole('option', { name: 'Admin' })
|
||||||
).click();
|
.click();
|
||||||
await adminCreateUserPage.createButton.click();
|
await adminCreateUserPage.createButton.click();
|
||||||
const snackbar = await adminUsersPage.getSnackbarData(
|
await adminCreateUserPage.expectCreateUserSuccessAlertToBeVisible();
|
||||||
'snackbar-create-user-success'
|
});
|
||||||
);
|
|
||||||
await expect(snackbar.variant).toBe('success');
|
|
||||||
await adminUsersPage.closeSnackbar();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
await test.step(
|
await test.step('Delete the created user', async () => {
|
||||||
'Delete the created user',
|
|
||||||
async () => {
|
|
||||||
await adminUsersPage.navigateTo();
|
await adminUsersPage.navigateTo();
|
||||||
await adminUsersPage.findUserPageWithEmail(testUser.email);
|
await adminUsersPage.findUserPageWithEmail(testUser.email);
|
||||||
const userRow = await adminUsersPage.getUserRowByEmail(testUser.email);
|
const userRow = await adminUsersPage.getUserRowByEmail(testUser.email);
|
||||||
@@ -142,127 +119,94 @@ test.describe('User management page', () => {
|
|||||||
await expect(snackbar.variant).toBe('success');
|
await expect(snackbar.variant).toBe('success');
|
||||||
await adminUsersPage.closeSnackbar();
|
await adminUsersPage.closeSnackbar();
|
||||||
await expect(userRow).not.toBeVisible(false);
|
await expect(userRow).not.toBeVisible(false);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
await test.step(
|
await test.step('Create the user again', async () => {
|
||||||
'Create the user again',
|
|
||||||
async () => {
|
|
||||||
await adminUsersPage.createUserButton.click();
|
await adminUsersPage.createUserButton.click();
|
||||||
await adminCreateUserPage.fullNameInput.fill(testUser.fullName);
|
await adminCreateUserPage.fullNameInput.fill(testUser.fullName);
|
||||||
await adminCreateUserPage.emailInput.fill(testUser.email);
|
await adminCreateUserPage.emailInput.fill(testUser.email);
|
||||||
await adminCreateUserPage.roleInput.click();
|
await adminCreateUserPage.roleInput.click();
|
||||||
await adminCreateUserPage.page.getByRole(
|
await adminCreateUserPage.page
|
||||||
'option', { name: 'Admin' }
|
.getByRole('option', { name: 'Admin' })
|
||||||
).click();
|
.click();
|
||||||
await adminCreateUserPage.createButton.click();
|
await adminCreateUserPage.createButton.click();
|
||||||
const snackbar = await adminUsersPage.getSnackbarData('snackbar-error');
|
await expect(adminCreateUserPage.fieldError).toHaveCount(1);
|
||||||
await expect(snackbar.variant).toBe('error');
|
});
|
||||||
await adminUsersPage.closeSnackbar();
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
test(
|
test('Creating a user which already exists', async ({
|
||||||
'Creating a user which already exists',
|
adminCreateUserPage,
|
||||||
async ({ adminCreateUserPage, adminUsersPage, page }) => {
|
adminUsersPage,
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
adminCreateUserPage.seed(9200);
|
adminCreateUserPage.seed(9200);
|
||||||
const testUser = adminCreateUserPage.generateUser();
|
const testUser = adminCreateUserPage.generateUser();
|
||||||
|
|
||||||
await test.step(
|
await test.step('Create the test user', async () => {
|
||||||
'Create the test user',
|
|
||||||
async () => {
|
|
||||||
await adminUsersPage.createUserButton.click();
|
await adminUsersPage.createUserButton.click();
|
||||||
await adminCreateUserPage.fullNameInput.fill(testUser.fullName);
|
await adminCreateUserPage.fullNameInput.fill(testUser.fullName);
|
||||||
await adminCreateUserPage.emailInput.fill(testUser.email);
|
await adminCreateUserPage.emailInput.fill(testUser.email);
|
||||||
await adminCreateUserPage.roleInput.click();
|
await adminCreateUserPage.roleInput.click();
|
||||||
await adminCreateUserPage.page.getByRole(
|
await adminCreateUserPage.page
|
||||||
'option', { name: 'Admin' }
|
.getByRole('option', { name: 'Admin' })
|
||||||
).click();
|
.click();
|
||||||
await adminCreateUserPage.createButton.click();
|
await adminCreateUserPage.createButton.click();
|
||||||
const snackbar = await adminUsersPage.getSnackbarData(
|
await adminCreateUserPage.expectCreateUserSuccessAlertToBeVisible();
|
||||||
'snackbar-create-user-success'
|
});
|
||||||
);
|
|
||||||
await expect(snackbar.variant).toBe('success');
|
|
||||||
await adminUsersPage.closeSnackbar();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
await test.step(
|
await test.step('Create the user again', async () => {
|
||||||
'Create the user again',
|
|
||||||
async () => {
|
|
||||||
await adminUsersPage.navigateTo();
|
await adminUsersPage.navigateTo();
|
||||||
await adminUsersPage.createUserButton.click();
|
await adminUsersPage.createUserButton.click();
|
||||||
await adminCreateUserPage.fullNameInput.fill(testUser.fullName);
|
await adminCreateUserPage.fullNameInput.fill(testUser.fullName);
|
||||||
await adminCreateUserPage.emailInput.fill(testUser.email);
|
await adminCreateUserPage.emailInput.fill(testUser.email);
|
||||||
const createUserPageUrl = page.url();
|
const createUserPageUrl = page.url();
|
||||||
await adminCreateUserPage.roleInput.click();
|
await adminCreateUserPage.roleInput.click();
|
||||||
await adminCreateUserPage.page.getByRole(
|
await adminCreateUserPage.page
|
||||||
'option', { name: 'Admin' }
|
.getByRole('option', { name: 'Admin' })
|
||||||
).click();
|
.click();
|
||||||
await adminCreateUserPage.createButton.click();
|
await adminCreateUserPage.createButton.click();
|
||||||
|
|
||||||
await expect(page.url()).toBe(createUserPageUrl);
|
await expect(page.url()).toBe(createUserPageUrl);
|
||||||
const snackbar = await adminUsersPage.getSnackbarData('snackbar-error');
|
await expect(adminCreateUserPage.fieldError).toHaveCount(1);
|
||||||
await expect(snackbar.variant).toBe('error');
|
});
|
||||||
await adminUsersPage.closeSnackbar();
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
test(
|
test('Editing a user to have the same email as another user should not be allowed', async ({
|
||||||
'Editing a user to have the same email as another user should not be allowed',
|
adminCreateUserPage,
|
||||||
async ({
|
adminEditUserPage,
|
||||||
adminCreateUserPage, adminEditUserPage, adminUsersPage, page
|
adminUsersPage,
|
||||||
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
adminCreateUserPage.seed(9300);
|
adminCreateUserPage.seed(9300);
|
||||||
const user1 = adminCreateUserPage.generateUser();
|
const user1 = adminCreateUserPage.generateUser();
|
||||||
const user2 = adminCreateUserPage.generateUser();
|
const user2 = adminCreateUserPage.generateUser();
|
||||||
await test.step(
|
await test.step('Create the first user', async () => {
|
||||||
'Create the first user',
|
|
||||||
async () => {
|
|
||||||
await adminUsersPage.navigateTo();
|
await adminUsersPage.navigateTo();
|
||||||
await adminUsersPage.createUserButton.click();
|
await adminUsersPage.createUserButton.click();
|
||||||
await adminCreateUserPage.fullNameInput.fill(user1.fullName);
|
await adminCreateUserPage.fullNameInput.fill(user1.fullName);
|
||||||
await adminCreateUserPage.emailInput.fill(user1.email);
|
await adminCreateUserPage.emailInput.fill(user1.email);
|
||||||
await adminCreateUserPage.roleInput.click();
|
await adminCreateUserPage.roleInput.click();
|
||||||
await adminCreateUserPage.page.getByRole(
|
await adminCreateUserPage.page
|
||||||
'option', { name: 'Admin' }
|
.getByRole('option', { name: 'Admin' })
|
||||||
).click();
|
.click();
|
||||||
await adminCreateUserPage.createButton.click();
|
await adminCreateUserPage.createButton.click();
|
||||||
const snackbar = await adminUsersPage.getSnackbarData(
|
await adminCreateUserPage.expectCreateUserSuccessAlertToBeVisible();
|
||||||
'snackbar-create-user-success'
|
});
|
||||||
);
|
|
||||||
await expect(snackbar.variant).toBe('success');
|
|
||||||
await adminUsersPage.closeSnackbar();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
await test.step(
|
await test.step('Create the second user', async () => {
|
||||||
'Create the second user',
|
|
||||||
async () => {
|
|
||||||
await adminUsersPage.navigateTo();
|
await adminUsersPage.navigateTo();
|
||||||
await adminUsersPage.createUserButton.click();
|
await adminUsersPage.createUserButton.click();
|
||||||
await adminCreateUserPage.fullNameInput.fill(user2.fullName);
|
await adminCreateUserPage.fullNameInput.fill(user2.fullName);
|
||||||
await adminCreateUserPage.emailInput.fill(user2.email);
|
await adminCreateUserPage.emailInput.fill(user2.email);
|
||||||
await adminCreateUserPage.roleInput.click();
|
await adminCreateUserPage.roleInput.click();
|
||||||
await adminCreateUserPage.page.getByRole(
|
await adminCreateUserPage.page
|
||||||
'option', { name: 'Admin' }
|
.getByRole('option', { name: 'Admin' })
|
||||||
).click();
|
.click();
|
||||||
await adminCreateUserPage.createButton.click();
|
await adminCreateUserPage.createButton.click();
|
||||||
const snackbar = await adminUsersPage.getSnackbarData(
|
await adminCreateUserPage.expectCreateUserSuccessAlertToBeVisible();
|
||||||
'snackbar-create-user-success'
|
});
|
||||||
);
|
|
||||||
await expect(snackbar.variant).toBe('success');
|
|
||||||
await adminUsersPage.closeSnackbar();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
await test.step(
|
await test.step('Try editing the second user to have the email of the first user', async () => {
|
||||||
'Try editing the second user to have the email of the first user',
|
|
||||||
async () => {
|
|
||||||
await adminUsersPage.navigateTo();
|
await adminUsersPage.navigateTo();
|
||||||
await adminUsersPage.findUserPageWithEmail(user2.email);
|
await adminUsersPage.findUserPageWithEmail(user2.email);
|
||||||
let userRow = await adminUsersPage.getUserRowByEmail(user2.email);
|
let userRow = await adminUsersPage.getUserRowByEmail(user2.email);
|
||||||
@@ -272,14 +216,10 @@ test.describe('User management page', () => {
|
|||||||
const editPageUrl = page.url();
|
const editPageUrl = page.url();
|
||||||
await adminEditUserPage.updateButton.click();
|
await adminEditUserPage.updateButton.click();
|
||||||
|
|
||||||
const snackbar = await adminUsersPage.getSnackbarData(
|
const snackbar = await adminUsersPage.getSnackbarData('snackbar-error');
|
||||||
'snackbar-error'
|
|
||||||
);
|
|
||||||
await expect(snackbar.variant).toBe('error');
|
await expect(snackbar.variant).toBe('error');
|
||||||
await adminUsersPage.closeSnackbar();
|
await adminUsersPage.closeSnackbar();
|
||||||
await expect(page.url()).toBe(editPageUrl);
|
await expect(page.url()).toBe(editPageUrl);
|
||||||
}
|
});
|
||||||
);
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
@@ -7,9 +7,10 @@ test('Ensure creating a new flow works', async ({ page }) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test(
|
test('Create a new flow with a Scheduler step then an Ntfy step', async ({
|
||||||
'Create a new flow with a Scheduler step then an Ntfy step',
|
flowEditorPage,
|
||||||
async ({ flowEditorPage, page }) => {
|
page,
|
||||||
|
}) => {
|
||||||
await test.step('create flow', async () => {
|
await test.step('create flow', async () => {
|
||||||
await test.step('navigate to new flow page', async () => {
|
await test.step('navigate to new flow page', async () => {
|
||||||
await page.getByTestId('create-flow-button').click();
|
await page.getByTestId('create-flow-button').click();
|
||||||
@@ -27,17 +28,13 @@ test(
|
|||||||
await test.step('choose app and event substep', async () => {
|
await test.step('choose app and event substep', async () => {
|
||||||
await test.step('choose application', async () => {
|
await test.step('choose application', async () => {
|
||||||
await flowEditorPage.appAutocomplete.click();
|
await flowEditorPage.appAutocomplete.click();
|
||||||
await page
|
await page.getByRole('option', { name: 'Scheduler' }).click();
|
||||||
.getByRole('option', { name: 'Scheduler' })
|
|
||||||
.click();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await test.step('choose and event', async () => {
|
await test.step('choose and event', async () => {
|
||||||
await expect(flowEditorPage.eventAutocomplete).toBeVisible();
|
await expect(flowEditorPage.eventAutocomplete).toBeVisible();
|
||||||
await flowEditorPage.eventAutocomplete.click();
|
await flowEditorPage.eventAutocomplete.click();
|
||||||
await page
|
await page.getByRole('option', { name: 'Every hour' }).click();
|
||||||
.getByRole('option', { name: 'Every hour' })
|
|
||||||
.click();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await test.step('continue to next step', async () => {
|
await test.step('continue to next step', async () => {
|
||||||
@@ -89,9 +86,7 @@ test(
|
|||||||
await test.step('choose an event', async () => {
|
await test.step('choose an event', async () => {
|
||||||
await expect(flowEditorPage.eventAutocomplete).toBeVisible();
|
await expect(flowEditorPage.eventAutocomplete).toBeVisible();
|
||||||
await flowEditorPage.eventAutocomplete.click();
|
await flowEditorPage.eventAutocomplete.click();
|
||||||
await page
|
await page.getByRole('option', { name: 'Send message' }).click();
|
||||||
.getByRole('option', { name: 'Send message' })
|
|
||||||
.click();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await test.step('continue to next step', async () => {
|
await test.step('continue to next step', async () => {
|
||||||
@@ -107,14 +102,18 @@ test(
|
|||||||
await test.step('choose connection substep', async () => {
|
await test.step('choose connection substep', async () => {
|
||||||
await test.step('choose connection list item', async () => {
|
await test.step('choose connection list item', async () => {
|
||||||
await flowEditorPage.connectionAutocomplete.click();
|
await flowEditorPage.connectionAutocomplete.click();
|
||||||
await page.getByRole('option').first().click();
|
await page
|
||||||
|
.getByRole('option')
|
||||||
|
.filter({ hasText: 'Add new connection' })
|
||||||
|
.click();
|
||||||
});
|
});
|
||||||
|
|
||||||
await test.step('continue to next step', async () => {
|
await test.step('continue to next step', async () => {
|
||||||
await flowEditorPage.continueButton.click();
|
await page.getByTestId('create-connection-button').click();
|
||||||
});
|
});
|
||||||
|
|
||||||
await test.step('collapses the substep', async () => {
|
await test.step('collapses the substep', async () => {
|
||||||
|
await flowEditorPage.continueButton.click();
|
||||||
await expect(flowEditorPage.connectionAutocomplete).not.toBeVisible();
|
await expect(flowEditorPage.connectionAutocomplete).not.toBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -143,10 +142,7 @@ test(
|
|||||||
await test.step('test trigger substep', async () => {
|
await test.step('test trigger substep', async () => {
|
||||||
await test.step('show sample output', async () => {
|
await test.step('show sample output', async () => {
|
||||||
await expect(flowEditorPage.testOutput).not.toBeVisible();
|
await expect(flowEditorPage.testOutput).not.toBeVisible();
|
||||||
await page
|
await page.getByTestId('flow-substep-continue-button').first().click();
|
||||||
.getByTestId('flow-substep-continue-button')
|
|
||||||
.first()
|
|
||||||
.click();
|
|
||||||
await expect(flowEditorPage.testOutput).toBeVisible();
|
await expect(flowEditorPage.testOutput).toBeVisible();
|
||||||
await flowEditorPage.screenshot({
|
await flowEditorPage.screenshot({
|
||||||
path: 'Ntfy action test output.png',
|
path: 'Ntfy action test output.png',
|
||||||
@@ -172,9 +168,7 @@ test(
|
|||||||
});
|
});
|
||||||
|
|
||||||
await test.step('unpublish from snackbar', async () => {
|
await test.step('unpublish from snackbar', async () => {
|
||||||
await page
|
await page.getByTestId('unpublish-flow-from-snackbar').click();
|
||||||
.getByTestId('unpublish-flow-from-snackbar')
|
|
||||||
.click();
|
|
||||||
await expect(flowEditorPage.infoSnackbar).not.toBeVisible();
|
await expect(flowEditorPage.infoSnackbar).not.toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -200,5 +194,4 @@ test(
|
|||||||
await expect(page).toHaveURL('/flows');
|
await expect(page).toHaveURL('/flows');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
@@ -33,10 +33,7 @@ publicTest.describe('My Profile', () => {
|
|||||||
.getByRole('option', { name: 'Admin' })
|
.getByRole('option', { name: 'Admin' })
|
||||||
.click();
|
.click();
|
||||||
await adminCreateUserPage.createButton.click();
|
await adminCreateUserPage.createButton.click();
|
||||||
const snackbar = await adminUsersPage.getSnackbarData(
|
await adminCreateUserPage.expectCreateUserSuccessAlertToBeVisible();
|
||||||
'snackbar-create-user-success'
|
|
||||||
);
|
|
||||||
await expect(snackbar.variant).toBe('success');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await publicTest.step('copy invitation link', async () => {
|
await publicTest.step('copy invitation link', async () => {
|
||||||
|
@@ -1,10 +1,19 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import MuiContainer from '@mui/material/Container';
|
import MuiContainer from '@mui/material/Container';
|
||||||
|
|
||||||
export default function Container(props) {
|
export default function Container({ maxWidth = 'lg', ...props }) {
|
||||||
return <MuiContainer {...props} />;
|
return <MuiContainer maxWidth={maxWidth} {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
Container.defaultProps = {
|
Container.propTypes = {
|
||||||
maxWidth: 'lg',
|
maxWidth: PropTypes.oneOf([
|
||||||
|
'xs',
|
||||||
|
'sm',
|
||||||
|
'md',
|
||||||
|
'lg',
|
||||||
|
'xl',
|
||||||
|
false,
|
||||||
|
PropTypes.string,
|
||||||
|
]),
|
||||||
};
|
};
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import Paper from '@mui/material/Paper';
|
import Paper from '@mui/material/Paper';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
|
import Alert from '@mui/material/Alert';
|
||||||
import LoadingButton from '@mui/lab/LoadingButton';
|
import LoadingButton from '@mui/lab/LoadingButton';
|
||||||
import { enqueueSnackbar } from 'notistack';
|
|
||||||
|
|
||||||
import useForgotPassword from 'hooks/useForgotPassword';
|
import useForgotPassword from 'hooks/useForgotPassword';
|
||||||
import Form from 'components/Form';
|
import Form from 'components/Form';
|
||||||
@@ -12,25 +12,17 @@ import useFormatMessage from 'hooks/useFormatMessage';
|
|||||||
export default function ForgotPasswordForm() {
|
export default function ForgotPasswordForm() {
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const {
|
const {
|
||||||
mutateAsync: forgotPassword,
|
mutate: forgotPassword,
|
||||||
isPending: loading,
|
isPending: loading,
|
||||||
isSuccess,
|
isSuccess,
|
||||||
|
isError,
|
||||||
|
error,
|
||||||
} = useForgotPassword();
|
} = useForgotPassword();
|
||||||
|
|
||||||
const handleSubmit = async (values) => {
|
const handleSubmit = ({ email }) => {
|
||||||
const { email } = values;
|
forgotPassword({
|
||||||
try {
|
|
||||||
await forgotPassword({
|
|
||||||
email,
|
email,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
|
||||||
enqueueSnackbar(
|
|
||||||
error?.message || formatMessage('forgotPasswordForm.error'),
|
|
||||||
{
|
|
||||||
variant: 'error',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -57,6 +49,16 @@ export default function ForgotPasswordForm() {
|
|||||||
margin="dense"
|
margin="dense"
|
||||||
autoComplete="username"
|
autoComplete="username"
|
||||||
/>
|
/>
|
||||||
|
{isError && (
|
||||||
|
<Alert severity="error" sx={{ mt: 2 }}>
|
||||||
|
{error?.message || formatMessage('forgotPasswordForm.error')}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
{isSuccess && (
|
||||||
|
<Alert severity="success" sx={{ mt: 2 }}>
|
||||||
|
{formatMessage('forgotPasswordForm.instructionsSent')}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
@@ -68,14 +70,6 @@ export default function ForgotPasswordForm() {
|
|||||||
>
|
>
|
||||||
{formatMessage('forgotPasswordForm.submit')}
|
{formatMessage('forgotPasswordForm.submit')}
|
||||||
</LoadingButton>
|
</LoadingButton>
|
||||||
{isSuccess && (
|
|
||||||
<Typography
|
|
||||||
variant="body1"
|
|
||||||
sx={{ color: (theme) => theme.palette.success.main }}
|
|
||||||
>
|
|
||||||
{formatMessage('forgotPasswordForm.instructionsSent')}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</Form>
|
</Form>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
|
@@ -46,7 +46,12 @@ function Form(props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
<form onSubmit={methods.handleSubmit(onSubmit)} {...formProps}>
|
<form
|
||||||
|
onSubmit={methods.handleSubmit((data, event) =>
|
||||||
|
onSubmit?.(data, event, methods.setError),
|
||||||
|
)}
|
||||||
|
{...formProps}
|
||||||
|
>
|
||||||
{render ? render(methods) : children}
|
{render ? render(methods) : children}
|
||||||
</form>
|
</form>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
|
@@ -2,6 +2,7 @@ import * as React from 'react';
|
|||||||
import { useNavigate, Link as RouterLink } from 'react-router-dom';
|
import { useNavigate, Link as RouterLink } from 'react-router-dom';
|
||||||
import Paper from '@mui/material/Paper';
|
import Paper from '@mui/material/Paper';
|
||||||
import Link from '@mui/material/Link';
|
import Link from '@mui/material/Link';
|
||||||
|
import Alert from '@mui/material/Alert';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import LoadingButton from '@mui/lab/LoadingButton';
|
import LoadingButton from '@mui/lab/LoadingButton';
|
||||||
import useAuthentication from 'hooks/useAuthentication';
|
import useAuthentication from 'hooks/useAuthentication';
|
||||||
@@ -11,7 +12,6 @@ import Form from 'components/Form';
|
|||||||
import TextField from 'components/TextField';
|
import TextField from 'components/TextField';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
import useCreateAccessToken from 'hooks/useCreateAccessToken';
|
import useCreateAccessToken from 'hooks/useCreateAccessToken';
|
||||||
import { Alert } from '@mui/material';
|
|
||||||
|
|
||||||
function LoginForm() {
|
function LoginForm() {
|
||||||
const isCloud = useCloud();
|
const isCloud = useCloud();
|
||||||
@@ -45,7 +45,7 @@ function LoginForm() {
|
|||||||
|
|
||||||
const renderError = () => {
|
const renderError = () => {
|
||||||
const errors = error?.response?.data?.errors?.general || [
|
const errors = error?.response?.data?.errors?.general || [
|
||||||
formatMessage('loginForm.error'),
|
error?.message || formatMessage('loginForm.error'),
|
||||||
];
|
];
|
||||||
|
|
||||||
return errors.map((error) => (
|
return errors.map((error) => (
|
||||||
|
@@ -2,6 +2,7 @@ import { yupResolver } from '@hookform/resolvers/yup';
|
|||||||
import LoadingButton from '@mui/lab/LoadingButton';
|
import LoadingButton from '@mui/lab/LoadingButton';
|
||||||
import Paper from '@mui/material/Paper';
|
import Paper from '@mui/material/Paper';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
|
import Alert from '@mui/material/Alert';
|
||||||
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
@@ -30,6 +31,8 @@ export default function ResetPasswordForm() {
|
|||||||
mutateAsync: resetPassword,
|
mutateAsync: resetPassword,
|
||||||
isPending,
|
isPending,
|
||||||
isSuccess,
|
isSuccess,
|
||||||
|
error,
|
||||||
|
isError,
|
||||||
} = useResetPassword();
|
} = useResetPassword();
|
||||||
const token = searchParams.get('token');
|
const token = searchParams.get('token');
|
||||||
|
|
||||||
@@ -47,14 +50,23 @@ export default function ResetPasswordForm() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
navigate(URLS.LOGIN);
|
navigate(URLS.LOGIN);
|
||||||
} catch (error) {
|
} catch {}
|
||||||
enqueueSnackbar(
|
};
|
||||||
error?.message || formatMessage('resetPasswordForm.error'),
|
|
||||||
{
|
const renderError = () => {
|
||||||
variant: 'error',
|
if (!isError) {
|
||||||
},
|
return null;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const errors = error?.response?.data?.errors?.general || [
|
||||||
|
error?.message || formatMessage('resetPasswordForm.error'),
|
||||||
|
];
|
||||||
|
|
||||||
|
return errors.map((error) => (
|
||||||
|
<Alert severity="error" sx={{ mt: 2 }}>
|
||||||
|
{error}
|
||||||
|
</Alert>
|
||||||
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -96,7 +108,6 @@ export default function ResetPasswordForm() {
|
|||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
label={formatMessage(
|
label={formatMessage(
|
||||||
'resetPasswordForm.confirmPasswordFieldLabel',
|
'resetPasswordForm.confirmPasswordFieldLabel',
|
||||||
@@ -117,7 +128,7 @@ export default function ResetPasswordForm() {
|
|||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
{renderError()}
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
|
18
packages/web/src/helpers/errors.js
Normal file
18
packages/web/src/helpers/errors.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
// Helpers to extract errors received from the API
|
||||||
|
|
||||||
|
export const getGeneralErrorMessage = ({ error, fallbackMessage }) => {
|
||||||
|
if (!error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const errors = error?.response?.data?.errors;
|
||||||
|
const generalError = errors?.general;
|
||||||
|
|
||||||
|
if (generalError && Array.isArray(generalError)) {
|
||||||
|
return generalError.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!errors) {
|
||||||
|
return error?.message || fallbackMessage;
|
||||||
|
}
|
||||||
|
};
|
@@ -225,6 +225,8 @@
|
|||||||
"userForm.email": "Email",
|
"userForm.email": "Email",
|
||||||
"userForm.role": "Role",
|
"userForm.role": "Role",
|
||||||
"userForm.password": "Password",
|
"userForm.password": "Password",
|
||||||
|
"userForm.mandatoryInput": "{inputName} is required.",
|
||||||
|
"userForm.validateEmail": "Email must be valid.",
|
||||||
"createUser.submit": "Create",
|
"createUser.submit": "Create",
|
||||||
"createUser.successfullyCreated": "The user has been created.",
|
"createUser.successfullyCreated": "The user has been created.",
|
||||||
"createUser.invitationEmailInfo": "Invitation email will be sent if SMTP credentials are valid. Otherwise, you can share the invitation link manually: <link></link>",
|
"createUser.invitationEmailInfo": "Invitation email will be sent if SMTP credentials are valid. Otherwise, you can share the invitation link manually: <link></link>",
|
||||||
@@ -249,8 +251,11 @@
|
|||||||
"createRolePage.title": "Create role",
|
"createRolePage.title": "Create role",
|
||||||
"roleForm.name": "Name",
|
"roleForm.name": "Name",
|
||||||
"roleForm.description": "Description",
|
"roleForm.description": "Description",
|
||||||
|
"roleForm.mandatoryInput": "{inputName} is required.",
|
||||||
"createRole.submit": "Create",
|
"createRole.submit": "Create",
|
||||||
"createRole.successfullyCreated": "The role has been created.",
|
"createRole.successfullyCreated": "The role has been created.",
|
||||||
|
"createRole.generalError": "Error while creating the role.",
|
||||||
|
"createRole.permissionsError": "Permissions are invalid.",
|
||||||
"editRole.submit": "Update",
|
"editRole.submit": "Update",
|
||||||
"editRole.successfullyUpdated": "The role has been updated.",
|
"editRole.successfullyUpdated": "The role has been updated.",
|
||||||
"roleList.name": "Name",
|
"roleList.name": "Name",
|
||||||
|
@@ -66,8 +66,8 @@ function RoleMappings({ provider, providerLoading }) {
|
|||||||
const enqueueSnackbar = useEnqueueSnackbar();
|
const enqueueSnackbar = useEnqueueSnackbar();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
mutateAsync: updateSamlAuthProvidersRoleMappings,
|
mutateAsync: updateRoleMappings,
|
||||||
isPending: isUpdateSamlAuthProvidersRoleMappingsPending,
|
isPending: isUpdateRoleMappingsPending,
|
||||||
} = useAdminUpdateSamlAuthProviderRoleMappings(provider?.id);
|
} = useAdminUpdateSamlAuthProviderRoleMappings(provider?.id);
|
||||||
|
|
||||||
const { data, isLoading: isAdminSamlAuthProviderRoleMappingsLoading } =
|
const { data, isLoading: isAdminSamlAuthProviderRoleMappingsLoading } =
|
||||||
@@ -79,7 +79,7 @@ function RoleMappings({ provider, providerLoading }) {
|
|||||||
const handleRoleMappingsUpdate = async (values) => {
|
const handleRoleMappingsUpdate = async (values) => {
|
||||||
try {
|
try {
|
||||||
if (provider?.id) {
|
if (provider?.id) {
|
||||||
await updateSamlAuthProvidersRoleMappings(
|
await updateRoleMappings(
|
||||||
values.roleMappings.map(({ roleId, remoteRoleName }) => ({
|
values.roleMappings.map(({ roleId, remoteRoleName }) => ({
|
||||||
roleId,
|
roleId,
|
||||||
remoteRoleName,
|
remoteRoleName,
|
||||||
@@ -148,7 +148,7 @@ function RoleMappings({ provider, providerLoading }) {
|
|||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
sx={{ boxShadow: 2 }}
|
sx={{ boxShadow: 2 }}
|
||||||
loading={isUpdateSamlAuthProvidersRoleMappingsPending}
|
loading={isUpdateRoleMappingsPending}
|
||||||
>
|
>
|
||||||
{formatMessage('roleMappingsForm.save')}
|
{formatMessage('roleMappingsForm.save')}
|
||||||
</LoadingButton>
|
</LoadingButton>
|
||||||
|
@@ -1,10 +1,14 @@
|
|||||||
import LoadingButton from '@mui/lab/LoadingButton';
|
import LoadingButton from '@mui/lab/LoadingButton';
|
||||||
import Grid from '@mui/material/Grid';
|
import Grid from '@mui/material/Grid';
|
||||||
import Stack from '@mui/material/Stack';
|
import Stack from '@mui/material/Stack';
|
||||||
|
import Alert from '@mui/material/Alert';
|
||||||
|
import AlertTitle from '@mui/material/AlertTitle';
|
||||||
import PermissionCatalogField from 'components/PermissionCatalogField/index.ee';
|
import PermissionCatalogField from 'components/PermissionCatalogField/index.ee';
|
||||||
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
|
import * as yup from 'yup';
|
||||||
|
|
||||||
import Container from 'components/Container';
|
import Container from 'components/Container';
|
||||||
import Form from 'components/Form';
|
import Form from 'components/Form';
|
||||||
@@ -15,10 +19,45 @@ import {
|
|||||||
getComputedPermissionsDefaultValues,
|
getComputedPermissionsDefaultValues,
|
||||||
getPermissions,
|
getPermissions,
|
||||||
} from 'helpers/computePermissions.ee';
|
} from 'helpers/computePermissions.ee';
|
||||||
|
import { getGeneralErrorMessage } from 'helpers/errors';
|
||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
import useAdminCreateRole from 'hooks/useAdminCreateRole';
|
import useAdminCreateRole from 'hooks/useAdminCreateRole';
|
||||||
import usePermissionCatalog from 'hooks/usePermissionCatalog.ee';
|
import usePermissionCatalog from 'hooks/usePermissionCatalog.ee';
|
||||||
|
|
||||||
|
const getValidationSchema = (formatMessage) => {
|
||||||
|
const getMandatoryFieldMessage = (fieldTranslationId) =>
|
||||||
|
formatMessage('roleForm.mandatoryInput', {
|
||||||
|
inputName: formatMessage(fieldTranslationId),
|
||||||
|
});
|
||||||
|
|
||||||
|
return yup.object().shape({
|
||||||
|
name: yup
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.required(getMandatoryFieldMessage('roleForm.name')),
|
||||||
|
description: yup.string().trim(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPermissionsErrorMessage = (error) => {
|
||||||
|
const errors = error?.response?.data?.errors;
|
||||||
|
|
||||||
|
if (errors) {
|
||||||
|
const permissionsErrors = Object.keys(errors)
|
||||||
|
.filter((key) => key.startsWith('permissions'))
|
||||||
|
.reduce((obj, key) => {
|
||||||
|
obj[key] = errors[key];
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
if (Object.keys(permissionsErrors).length > 0) {
|
||||||
|
return JSON.stringify(permissionsErrors, null, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
export default function CreateRole() {
|
export default function CreateRole() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
@@ -41,7 +80,7 @@ export default function CreateRole() {
|
|||||||
[permissionCatalogData],
|
[permissionCatalogData],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleRoleCreation = async (roleData) => {
|
const handleRoleCreation = async (roleData, e, setError) => {
|
||||||
try {
|
try {
|
||||||
const permissions = getPermissions(roleData.computedPermissions);
|
const permissions = getPermissions(roleData.computedPermissions);
|
||||||
|
|
||||||
@@ -60,14 +99,38 @@ export default function CreateRole() {
|
|||||||
|
|
||||||
navigate(URLS.ROLES);
|
navigate(URLS.ROLES);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errors = Object.values(error.response.data.errors);
|
const errors = error?.response?.data?.errors;
|
||||||
|
|
||||||
for (const [errorMessage] of errors) {
|
if (errors) {
|
||||||
enqueueSnackbar(errorMessage, {
|
const fieldNames = ['name', 'description'];
|
||||||
variant: 'error',
|
Object.entries(errors).forEach(([fieldName, fieldErrors]) => {
|
||||||
SnackbarProps: {
|
if (fieldNames.includes(fieldName) && Array.isArray(fieldErrors)) {
|
||||||
'data-test': 'snackbar-error',
|
setError(fieldName, {
|
||||||
},
|
type: 'fieldRequestError',
|
||||||
|
message: fieldErrors.join(', '),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const permissionError = getPermissionsErrorMessage(error);
|
||||||
|
|
||||||
|
if (permissionError) {
|
||||||
|
setError('root.permissions', {
|
||||||
|
type: 'fieldRequestError',
|
||||||
|
message: permissionError,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const generalError = getGeneralErrorMessage({
|
||||||
|
error,
|
||||||
|
fallbackMessage: formatMessage('createRole.generalError'),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (generalError) {
|
||||||
|
setError('root.general', {
|
||||||
|
type: 'requestError',
|
||||||
|
message: generalError,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -83,7 +146,15 @@ export default function CreateRole() {
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid item xs={12} justifyContent="flex-end" sx={{ pt: 5 }}>
|
<Grid item xs={12} justifyContent="flex-end" sx={{ pt: 5 }}>
|
||||||
<Form onSubmit={handleRoleCreation} defaultValues={defaultValues}>
|
<Form
|
||||||
|
onSubmit={handleRoleCreation}
|
||||||
|
defaultValues={defaultValues}
|
||||||
|
noValidate
|
||||||
|
resolver={yupResolver(
|
||||||
|
getValidationSchema(formatMessage, defaultValues),
|
||||||
|
)}
|
||||||
|
automaticValidation={false}
|
||||||
|
render={({ formState: { errors } }) => (
|
||||||
<Stack direction="column" gap={2}>
|
<Stack direction="column" gap={2}>
|
||||||
<TextField
|
<TextField
|
||||||
required={true}
|
required={true}
|
||||||
@@ -91,6 +162,8 @@ export default function CreateRole() {
|
|||||||
label={formatMessage('roleForm.name')}
|
label={formatMessage('roleForm.name')}
|
||||||
fullWidth
|
fullWidth
|
||||||
data-test="name-input"
|
data-test="name-input"
|
||||||
|
error={!!errors?.name}
|
||||||
|
helperText={errors?.name?.message}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
@@ -98,10 +171,29 @@ export default function CreateRole() {
|
|||||||
label={formatMessage('roleForm.description')}
|
label={formatMessage('roleForm.description')}
|
||||||
fullWidth
|
fullWidth
|
||||||
data-test="description-input"
|
data-test="description-input"
|
||||||
|
error={!!errors?.description}
|
||||||
|
helperText={errors?.description?.message}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PermissionCatalogField name="computedPermissions" />
|
<PermissionCatalogField name="computedPermissions" />
|
||||||
|
|
||||||
|
{errors?.root?.permissions && (
|
||||||
|
<Alert severity="error" data-test="create-role-error-alert">
|
||||||
|
<AlertTitle>
|
||||||
|
{formatMessage('createRole.permissionsError')}
|
||||||
|
</AlertTitle>
|
||||||
|
<pre>
|
||||||
|
<code>{errors?.root?.permissions?.message}</code>
|
||||||
|
</pre>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{errors?.root?.general && (
|
||||||
|
<Alert severity="error" data-test="create-role-error-alert">
|
||||||
|
{errors?.root?.general?.message}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
@@ -113,7 +205,8 @@ export default function CreateRole() {
|
|||||||
{formatMessage('createRole.submit')}
|
{formatMessage('createRole.submit')}
|
||||||
</LoadingButton>
|
</LoadingButton>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Form>
|
)}
|
||||||
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Container>
|
</Container>
|
||||||
|
@@ -3,9 +3,10 @@ import Grid from '@mui/material/Grid';
|
|||||||
import Stack from '@mui/material/Stack';
|
import Stack from '@mui/material/Stack';
|
||||||
import Alert from '@mui/material/Alert';
|
import Alert from '@mui/material/Alert';
|
||||||
import MuiTextField from '@mui/material/TextField';
|
import MuiTextField from '@mui/material/TextField';
|
||||||
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import * as yup from 'yup';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
|
|
||||||
import Can from 'components/Can';
|
import Can from 'components/Can';
|
||||||
import Container from 'components/Container';
|
import Container from 'components/Container';
|
||||||
@@ -16,50 +17,94 @@ import TextField from 'components/TextField';
|
|||||||
import useFormatMessage from 'hooks/useFormatMessage';
|
import useFormatMessage from 'hooks/useFormatMessage';
|
||||||
import useRoles from 'hooks/useRoles.ee';
|
import useRoles from 'hooks/useRoles.ee';
|
||||||
import useAdminCreateUser from 'hooks/useAdminCreateUser';
|
import useAdminCreateUser from 'hooks/useAdminCreateUser';
|
||||||
|
import useCurrentUserAbility from 'hooks/useCurrentUserAbility';
|
||||||
|
import { getGeneralErrorMessage } from 'helpers/errors';
|
||||||
|
|
||||||
function generateRoleOptions(roles) {
|
function generateRoleOptions(roles) {
|
||||||
return roles?.map(({ name: label, id: value }) => ({ label, value }));
|
return roles?.map(({ name: label, id: value }) => ({ label, value }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getValidationSchema = (formatMessage, canUpdateRole) => {
|
||||||
|
const getMandatoryFieldMessage = (fieldTranslationId) =>
|
||||||
|
formatMessage('userForm.mandatoryInput', {
|
||||||
|
inputName: formatMessage(fieldTranslationId),
|
||||||
|
});
|
||||||
|
|
||||||
|
return yup.object().shape({
|
||||||
|
fullName: yup
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.required(getMandatoryFieldMessage('userForm.fullName')),
|
||||||
|
email: yup
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.email(formatMessage('userForm.validateEmail'))
|
||||||
|
.required(getMandatoryFieldMessage('userForm.email')),
|
||||||
|
...(canUpdateRole
|
||||||
|
? {
|
||||||
|
roleId: yup
|
||||||
|
.string()
|
||||||
|
.required(getMandatoryFieldMessage('userForm.role')),
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultValues = {
|
||||||
|
fullName: '',
|
||||||
|
email: '',
|
||||||
|
roleId: '',
|
||||||
|
};
|
||||||
|
|
||||||
export default function CreateUser() {
|
export default function CreateUser() {
|
||||||
const formatMessage = useFormatMessage();
|
const formatMessage = useFormatMessage();
|
||||||
const {
|
const {
|
||||||
mutateAsync: createUser,
|
mutateAsync: createUser,
|
||||||
isPending: isCreateUserPending,
|
isPending: isCreateUserPending,
|
||||||
data: createdUser,
|
data: createdUser,
|
||||||
|
isSuccess: createUserSuccess,
|
||||||
} = useAdminCreateUser();
|
} = useAdminCreateUser();
|
||||||
const { data: rolesData, loading: isRolesLoading } = useRoles();
|
const { data: rolesData, loading: isRolesLoading } = useRoles();
|
||||||
const roles = rolesData?.data;
|
const roles = rolesData?.data;
|
||||||
const enqueueSnackbar = useEnqueueSnackbar();
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const currentUserAbility = useCurrentUserAbility();
|
||||||
|
const canUpdateRole = currentUserAbility.can('update', 'Role');
|
||||||
|
|
||||||
const handleUserCreation = async (userData) => {
|
const handleUserCreation = async (userData, e, setError) => {
|
||||||
try {
|
try {
|
||||||
await createUser({
|
await createUser({
|
||||||
fullName: userData.fullName,
|
fullName: userData.fullName,
|
||||||
email: userData.email,
|
email: userData.email,
|
||||||
roleId: userData.role?.id,
|
roleId: userData.roleId,
|
||||||
});
|
});
|
||||||
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['admin', 'users'] });
|
queryClient.invalidateQueries({ queryKey: ['admin', 'users'] });
|
||||||
|
|
||||||
enqueueSnackbar(formatMessage('createUser.successfullyCreated'), {
|
|
||||||
variant: 'success',
|
|
||||||
persist: true,
|
|
||||||
SnackbarProps: {
|
|
||||||
'data-test': 'snackbar-create-user-success',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
enqueueSnackbar(formatMessage('createUser.error'), {
|
const errors = error?.response?.data?.errors;
|
||||||
variant: 'error',
|
|
||||||
persist: true,
|
if (errors) {
|
||||||
SnackbarProps: {
|
const fieldNames = Object.keys(defaultValues);
|
||||||
'data-test': 'snackbar-error',
|
Object.entries(errors).forEach(([fieldName, fieldErrors]) => {
|
||||||
},
|
if (fieldNames.includes(fieldName) && Array.isArray(fieldErrors)) {
|
||||||
|
setError(fieldName, {
|
||||||
|
type: 'fieldRequestError',
|
||||||
|
message: fieldErrors.join(', '),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const generalError = getGeneralErrorMessage({
|
||||||
|
error,
|
||||||
|
fallbackMessage: formatMessage('createUser.error'),
|
||||||
});
|
});
|
||||||
|
|
||||||
throw new Error('Failed while creating!');
|
if (generalError) {
|
||||||
|
setError('root.general', {
|
||||||
|
type: 'requestError',
|
||||||
|
message: generalError,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -73,7 +118,16 @@ export default function CreateUser() {
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid item xs={12} justifyContent="flex-end" sx={{ pt: 5 }}>
|
<Grid item xs={12} justifyContent="flex-end" sx={{ pt: 5 }}>
|
||||||
<Form onSubmit={handleUserCreation}>
|
<Form
|
||||||
|
noValidate
|
||||||
|
onSubmit={handleUserCreation}
|
||||||
|
mode="onSubmit"
|
||||||
|
defaultValues={defaultValues}
|
||||||
|
resolver={yupResolver(
|
||||||
|
getValidationSchema(formatMessage, canUpdateRole),
|
||||||
|
)}
|
||||||
|
automaticValidation={false}
|
||||||
|
render={({ formState: { errors } }) => (
|
||||||
<Stack direction="column" gap={2}>
|
<Stack direction="column" gap={2}>
|
||||||
<TextField
|
<TextField
|
||||||
required={true}
|
required={true}
|
||||||
@@ -81,6 +135,8 @@ export default function CreateUser() {
|
|||||||
label={formatMessage('userForm.fullName')}
|
label={formatMessage('userForm.fullName')}
|
||||||
data-test="full-name-input"
|
data-test="full-name-input"
|
||||||
fullWidth
|
fullWidth
|
||||||
|
error={!!errors?.fullName}
|
||||||
|
helperText={errors?.fullName?.message}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
@@ -89,11 +145,13 @@ export default function CreateUser() {
|
|||||||
label={formatMessage('userForm.email')}
|
label={formatMessage('userForm.email')}
|
||||||
data-test="email-input"
|
data-test="email-input"
|
||||||
fullWidth
|
fullWidth
|
||||||
|
error={!!errors?.email}
|
||||||
|
helperText={errors?.email?.message}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Can I="update" a="Role">
|
<Can I="update" a="Role">
|
||||||
<ControlledAutocomplete
|
<ControlledAutocomplete
|
||||||
name="role.id"
|
name="roleId"
|
||||||
fullWidth
|
fullWidth
|
||||||
disablePortal
|
disablePortal
|
||||||
disableClearable={true}
|
disableClearable={true}
|
||||||
@@ -103,28 +161,40 @@ export default function CreateUser() {
|
|||||||
{...params}
|
{...params}
|
||||||
required
|
required
|
||||||
label={formatMessage('userForm.role')}
|
label={formatMessage('userForm.role')}
|
||||||
|
error={!!errors?.roleId}
|
||||||
|
helperText={errors?.roleId?.message}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
loading={isRolesLoading}
|
loading={isRolesLoading}
|
||||||
|
showHelperText={false}
|
||||||
/>
|
/>
|
||||||
</Can>
|
</Can>
|
||||||
|
|
||||||
<LoadingButton
|
{errors?.root?.general && (
|
||||||
type="submit"
|
<Alert data-test="create-user-error-alert" severity="error">
|
||||||
variant="contained"
|
{errors?.root?.general?.message}
|
||||||
color="primary"
|
</Alert>
|
||||||
sx={{ boxShadow: 2 }}
|
)}
|
||||||
loading={isCreateUserPending}
|
|
||||||
data-test="create-button"
|
{createUserSuccess && (
|
||||||
|
<Alert
|
||||||
|
severity="success"
|
||||||
|
data-test="create-user-success-alert"
|
||||||
>
|
>
|
||||||
{formatMessage('createUser.submit')}
|
{formatMessage('createUser.successfullyCreated')}
|
||||||
</LoadingButton>
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
{createdUser && (
|
{createdUser && (
|
||||||
<Alert
|
<Alert
|
||||||
severity="info"
|
severity="info"
|
||||||
color="primary"
|
color="primary"
|
||||||
data-test="invitation-email-info-alert"
|
data-test="invitation-email-info-alert"
|
||||||
|
sx={{
|
||||||
|
a: {
|
||||||
|
wordBreak: 'break-all',
|
||||||
|
},
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{formatMessage('createUser.invitationEmailInfo', {
|
{formatMessage('createUser.invitationEmailInfo', {
|
||||||
link: () => (
|
link: () => (
|
||||||
@@ -139,8 +209,20 @@ export default function CreateUser() {
|
|||||||
})}
|
})}
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<LoadingButton
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
sx={{ boxShadow: 2 }}
|
||||||
|
loading={isCreateUserPending}
|
||||||
|
data-test="create-button"
|
||||||
|
>
|
||||||
|
{formatMessage('createUser.submit')}
|
||||||
|
</LoadingButton>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Form>
|
)}
|
||||||
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Container>
|
</Container>
|
||||||
|
Reference in New Issue
Block a user